Skip to content

Commit 0480c40

Browse files
committed
Refactor StreamFlattener to use EntityPathTracker
Removes the logic for tracking the entity path from the `StreamFlattener` module as it is now provided by the `EntityPathTracker`.
1 parent 0a3ff6f commit 0480c40

File tree

2 files changed

+207
-54
lines changed

2 files changed

+207
-54
lines changed
Lines changed: 65 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,126 @@
11
/*
2-
* Copyright 2013, 2014 Deutsche Nationalbibliothek
2+
* Copyright 2016 Deutsche Nationalbibliothek
33
*
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
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
77
*
8-
* http://www.apache.org/licenses/LICENSE-2.0
8+
* http://www.apache.org/licenses/LICENSE-2.0
99
*
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.
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.
1515
*/
16-
package org.culturegraph.mf.stream.pipe;
1716

18-
import java.util.Deque;
19-
import java.util.LinkedList;
20-
import java.util.NoSuchElementException;
17+
package org.culturegraph.mf.stream.pipe;
2118

2219
import org.culturegraph.mf.framework.DefaultStreamPipe;
2320
import org.culturegraph.mf.framework.StreamReceiver;
2421
import org.culturegraph.mf.framework.annotations.Description;
2522
import org.culturegraph.mf.framework.annotations.In;
2623
import org.culturegraph.mf.framework.annotations.Out;
27-
24+
import org.culturegraph.mf.stream.sink.EntityPathTracker;
2825

2926
/**
30-
* flattens out entities in a stream by introducing dots in literal names.
27+
* Flattens all entities in a stream by prefixing the literals with the entity
28+
* paths. The stream emitted by this module is guaranteed to not contain any
29+
* <i>start-entity</i> and <i>end-entity</i> events.
30+
*
31+
* <p>For example, take the following sequence of events:
32+
* <pre>{@literal
33+
* start-record "1"
34+
* literal "toplevel": literal-value
35+
* start-entity "entity"
36+
* literal "nested": literal-value
37+
* end-entity
38+
* end-record
39+
* }</pre>
40+
*
41+
* These events are transformed by the {@code StreamFlattener} into the
42+
* following sequence of events:
43+
* <pre>{@literal
44+
* start-record "1"
45+
* literal "toplevel": literal-value
46+
* literal "entity.nested": literal-value
47+
* end-record
48+
* }</pre>
49+
*
50+
* @author Christoph Böhme (rewrite)
3151
* @author Markus Michael Geipel
32-
*
52+
* @see EntityPathTracker
3353
*/
34-
3554
@Description("flattens out entities in a stream by introducing dots in literal names")
3655
@In(StreamReceiver.class)
3756
@Out(StreamReceiver.class)
3857
public final class StreamFlattener extends DefaultStreamPipe<StreamReceiver> {
39-
58+
4059
public static final String DEFAULT_ENTITY_MARKER = ".";
41-
private static final String ENTITIES_NOT_BALANCED = "Entity starts and ends are not balanced";
4260

43-
private String entityMarker = DEFAULT_ENTITY_MARKER;
44-
private final Deque<String> entityStack = new LinkedList<String>();
45-
private final StringBuilder entityPath = new StringBuilder();
46-
private String currentEntityPath = "";
47-
48-
public void setEntityMarker(final String entityMarker) {
49-
this.entityMarker = entityMarker;
61+
private static final String ENTITIES_NOT_BALANCED =
62+
"Entity starts and ends are not balanced";
63+
64+
private final EntityPathTracker pathTracker = new EntityPathTracker();
65+
66+
public StreamFlattener() {
67+
setEntityMarker(DEFAULT_ENTITY_MARKER);
5068
}
5169

5270
public String getEntityMarker() {
53-
return entityMarker;
71+
return pathTracker.getEntitySeparator();
72+
}
73+
74+
public void setEntityMarker(final String entityMarker) {
75+
pathTracker.setEntitySeparator(entityMarker);
5476
}
5577

5678
@Override
5779
public void startRecord(final String identifier) {
5880
assert !isClosed();
59-
entityStack.clear();
60-
currentEntityPath = "";
61-
if (entityPath.length() != 0) {
62-
entityPath.delete(0, entityPath.length());
63-
}
81+
pathTracker.startRecord(identifier);
6482
getReceiver().startRecord(identifier);
6583
}
6684

6785
@Override
6886
public void endRecord() {
6987
assert !isClosed();
70-
currentEntityPath = "";
88+
if (pathTracker.getCurrentEntityName() != null) {
89+
// TODO: Remove this check in 4.0.0. We assume well-formedness
90+
throw new IllegalStateException(ENTITIES_NOT_BALANCED);
91+
}
92+
pathTracker.endRecord();
7193
getReceiver().endRecord();
72-
7394
}
7495

7596
@Override
7697
public void startEntity(final String name) {
7798
assert !isClosed();
78-
entityStack.push(name);
79-
entityPath.append(name);
80-
entityPath.append(entityMarker);
81-
currentEntityPath = entityPath.toString();
82-
99+
pathTracker.startEntity(name);
83100
}
84101

85102
@Override
86103
public void endEntity() {
87104
assert !isClosed();
88-
try {
89-
final int end = entityPath.length();
90-
final String name = entityStack.pop();
91-
entityPath.delete(end - name.length() - entityMarker.length(), end);
92-
currentEntityPath = entityPath.toString();
93-
} catch (NoSuchElementException exc) {
94-
throw new IllegalStateException(ENTITIES_NOT_BALANCED + ": " + exc.getMessage(), exc);
105+
if (pathTracker.getCurrentEntityName() == null) {
106+
// TODO: Remove this check in 4.0.0. We assume well-formedness
107+
throw new IllegalStateException(ENTITIES_NOT_BALANCED);
95108
}
109+
pathTracker.endEntity();
96110
}
97111

98112
@Override
99113
public void literal(final String name, final String value) {
100114
assert !isClosed();
101-
getReceiver().literal(currentEntityPath + name, value);
115+
getReceiver().literal(pathTracker.getCurrentPathWith(name), value);
102116
}
103117

104118
public String getCurrentEntityName() {
105-
return entityStack.peek();
119+
return pathTracker.getCurrentEntityName();
106120
}
107121

108122
public String getCurrentPath() {
109-
if(currentEntityPath.isEmpty()){
110-
return "";
111-
}
112-
return currentEntityPath.substring(0, currentEntityPath.length() - entityMarker.length());
123+
return pathTracker.getCurrentPath();
113124
}
114-
125+
115126
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Copyright 2016 Deutsche Nationalbibliothek
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+
17+
package org.culturegraph.mf.stream.pipe;
18+
19+
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.assertNull;
21+
import static org.mockito.Mockito.inOrder;
22+
23+
import org.culturegraph.mf.framework.StreamReceiver;
24+
import org.junit.Before;
25+
import org.junit.Test;
26+
import org.mockito.InOrder;
27+
import org.mockito.Mock;
28+
import org.mockito.MockitoAnnotations;
29+
30+
/**
31+
* @author Christoph Böhme
32+
*/
33+
public class StreamFlattenerTest {
34+
35+
@Mock
36+
private StreamReceiver receiver;
37+
38+
private StreamFlattener flattener;
39+
40+
@Before
41+
public void init() {
42+
MockitoAnnotations.initMocks(this);
43+
flattener = new StreamFlattener();
44+
flattener.setReceiver(receiver);
45+
}
46+
47+
@Test
48+
public void shouldFlattenEntitiesAndUseEntityPathForLiteralNames() {
49+
flattener.startRecord("1");
50+
flattener.startEntity("granny");
51+
flattener.literal("me", "value1");
52+
flattener.startEntity("mommy");
53+
flattener.literal("myself", "value2");
54+
flattener.endEntity();
55+
flattener.endEntity();
56+
flattener.literal("andI", "value3");
57+
flattener.endRecord();
58+
flattener.closeStream();
59+
60+
final InOrder ordered = inOrder(receiver);
61+
ordered.verify(receiver).startRecord("1");
62+
ordered.verify(receiver).literal("granny.me", "value1");
63+
ordered.verify(receiver).literal("granny.mommy.myself", "value2");
64+
ordered.verify(receiver).literal("andI", "value3");
65+
ordered.verify(receiver).endRecord();
66+
ordered.verify(receiver).closeStream();
67+
}
68+
69+
@Test
70+
public void getCurrentPath_shouldReturnPathToCurrentEntity() {
71+
flattener.startRecord("1");
72+
assertEquals("", flattener.getCurrentPath());
73+
flattener.startEntity("granny");
74+
assertEquals("granny", flattener.getCurrentPath());
75+
flattener.literal("me", "value1");
76+
assertEquals("granny", flattener.getCurrentPath());
77+
flattener.startEntity("mommy");
78+
assertEquals("granny.mommy", flattener.getCurrentPath());
79+
flattener.literal("myself", "value2");
80+
assertEquals("granny.mommy", flattener.getCurrentPath());
81+
flattener.endEntity();
82+
assertEquals("granny", flattener.getCurrentPath());
83+
flattener.endEntity();
84+
assertEquals("", flattener.getCurrentPath());
85+
flattener.literal("andI", "value3");
86+
assertEquals("", flattener.getCurrentPath());
87+
flattener.endRecord();
88+
assertEquals("", flattener.getCurrentPath());
89+
flattener.closeStream();
90+
}
91+
92+
@Test
93+
public void getCurrentEntityName_shouldReturnNameOfCurrentEntity() {
94+
flattener.startRecord("1");
95+
assertNull(flattener.getCurrentEntityName());
96+
flattener.startEntity("granny");
97+
assertEquals("granny", flattener.getCurrentEntityName());
98+
flattener.literal("me", "value1");
99+
assertEquals("granny", flattener.getCurrentEntityName());
100+
flattener.startEntity("mommy");
101+
assertEquals("mommy", flattener.getCurrentEntityName());
102+
flattener.literal("myself", "value2");
103+
assertEquals("mommy", flattener.getCurrentEntityName());
104+
flattener.endEntity();
105+
assertEquals("granny", flattener.getCurrentEntityName());
106+
flattener.endEntity();
107+
assertNull(flattener.getCurrentEntityName());
108+
flattener.literal("andI", "value3");
109+
assertNull(flattener.getCurrentEntityName());
110+
flattener.endRecord();
111+
assertNull(flattener.getCurrentEntityName());
112+
flattener.closeStream();
113+
}
114+
115+
@Test
116+
public void setEntityMarker_shouldChangeMarkerBetweenEntities() {
117+
flattener.setEntityMarker("-");
118+
119+
flattener.startRecord("1");
120+
flattener.startEntity("granny");
121+
flattener.startEntity("mommy");
122+
assertEquals("granny-mommy", flattener.getCurrentPath());
123+
}
124+
125+
@Test(expected = IllegalStateException.class)
126+
public void endEntity_shouldThrowExceptionIfEventsAreNotBalanced() {
127+
flattener.startRecord("1");
128+
flattener.startEntity("entity");
129+
flattener.endEntity();
130+
flattener.endEntity(); // should throw exception
131+
}
132+
133+
@Test(expected = IllegalStateException.class)
134+
public void endRecord_shouldThrowExceptionIfEventsAreNotBalanced() {
135+
flattener.startRecord("1");
136+
flattener.startEntity("entity1");
137+
flattener.startEntity("entity2");
138+
flattener.endEntity();
139+
flattener.endRecord(); // should throw exception
140+
}
141+
142+
}

0 commit comments

Comments
 (0)