begin of MARC parsing in JSON
This commit is contained in:
parent
dceb103fcb
commit
e225155e68
15 changed files with 576 additions and 14 deletions
|
@ -1,5 +1,5 @@
|
|||
group = org.xbib
|
||||
name = marc
|
||||
version = 2.9.20
|
||||
version = 2.9.21
|
||||
|
||||
org.gradle.warning.mode = ALL
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package org.xbib.marc;
|
||||
|
||||
import java.util.Objects;
|
||||
import org.xbib.marc.label.RecordLabel;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
@ -44,7 +45,7 @@ import static org.xbib.marc.json.MarcJsonWriter.LEADER_TAG;
|
|||
import static org.xbib.marc.json.MarcJsonWriter.TYPE_TAG;
|
||||
|
||||
/**
|
||||
* A MARC record. This is an extended MARC record augmented with MarcXchange information.
|
||||
* A MARC record.
|
||||
*/
|
||||
public class MarcRecord implements Map<String, Object> {
|
||||
|
||||
|
@ -91,9 +92,7 @@ public class MarcRecord implements Map<String, Object> {
|
|||
this.format = format;
|
||||
this.type = type;
|
||||
this.recordLabel = recordLabel;
|
||||
if (recordLabel == null) {
|
||||
throw new NullPointerException("record label must not be null");
|
||||
}
|
||||
Objects.requireNonNull(recordLabel, "record label must not be null");
|
||||
this.marcFields = marcFields;
|
||||
this.delegate = lightweight ? Map.of() : createMapFromMarcFields(comparator);
|
||||
}
|
||||
|
@ -179,6 +178,15 @@ public class MarcRecord implements Map<String, Object> {
|
|||
return recordLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the MARC fields of this record.
|
||||
*
|
||||
* @return the MARC field list
|
||||
*/
|
||||
public List<MarcField> getFields() {
|
||||
return marcFields;
|
||||
}
|
||||
|
||||
public LocalDate getCreationDate(LocalDate defaultDate) {
|
||||
if (marcFields != null) {
|
||||
MarcField marcField = getFirst("008");
|
||||
|
@ -255,15 +263,6 @@ public class MarcRecord implements Map<String, Object> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the MARC fields of this record.
|
||||
*
|
||||
* @return the MARC field list
|
||||
*/
|
||||
public List<MarcField> getFields() {
|
||||
return marcFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter all MARC fields of this record with a given tag.
|
||||
*
|
||||
|
|
13
src/main/java/org/xbib/marc/json/JsonException.java
Normal file
13
src/main/java/org/xbib/marc/json/JsonException.java
Normal file
|
@ -0,0 +1,13 @@
|
|||
package org.xbib.marc.json;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public class JsonException extends RuntimeException {
|
||||
|
||||
public JsonException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public JsonException(Exception exception) {
|
||||
super(exception);
|
||||
}
|
||||
}
|
30
src/main/java/org/xbib/marc/json/JsonListener.java
Normal file
30
src/main/java/org/xbib/marc/json/JsonListener.java
Normal file
|
@ -0,0 +1,30 @@
|
|||
package org.xbib.marc.json;
|
||||
|
||||
public interface JsonListener {
|
||||
|
||||
void begin();
|
||||
|
||||
void end();
|
||||
|
||||
void onNull();
|
||||
|
||||
void onTrue();
|
||||
|
||||
void onFalse();
|
||||
|
||||
void onKey(CharSequence key);
|
||||
|
||||
void onValue(CharSequence value);
|
||||
|
||||
void onLong(Long value);
|
||||
|
||||
void onDouble(Double value);
|
||||
|
||||
void beginCollection();
|
||||
|
||||
void endCollection();
|
||||
|
||||
void beginMap();
|
||||
|
||||
void endMap();
|
||||
}
|
291
src/main/java/org/xbib/marc/json/JsonParser.java
Normal file
291
src/main/java/org/xbib/marc/json/JsonParser.java
Normal file
|
@ -0,0 +1,291 @@
|
|||
package org.xbib.marc.json;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.util.Objects;
|
||||
|
||||
public class JsonParser implements Parser {
|
||||
|
||||
private final JsonResultListener listener;
|
||||
|
||||
private Reader reader;
|
||||
|
||||
private int ch;
|
||||
|
||||
public JsonParser() {
|
||||
this(new MarcJsonListener());
|
||||
}
|
||||
|
||||
public JsonParser(JsonResultListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node<?> parse(Reader reader) throws IOException {
|
||||
Objects.requireNonNull(reader);
|
||||
Objects.requireNonNull(listener);
|
||||
this.reader = reader;
|
||||
listener.begin();
|
||||
ch = reader.read();
|
||||
skipWhitespace();
|
||||
parseValue();
|
||||
skipWhitespace();
|
||||
if (ch != -1) {
|
||||
throw new JsonException("malformed json: " + ch);
|
||||
}
|
||||
listener.end();
|
||||
return listener.getResult();
|
||||
}
|
||||
|
||||
private void parseValue() throws IOException, JsonException {
|
||||
switch (ch) {
|
||||
case '"' -> parseString(false);
|
||||
case '{' -> parseMap();
|
||||
case '[' -> parseList();
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-' -> parseNumber();
|
||||
case 't' -> parseTrue();
|
||||
case 'f' -> parseFalse();
|
||||
case 'n' -> parseNull();
|
||||
default -> throw new JsonException("illegal character: " + ch);
|
||||
}
|
||||
}
|
||||
|
||||
private void parseNumber() throws IOException, JsonException {
|
||||
boolean minus = false;
|
||||
boolean dot = false;
|
||||
boolean exponent = false;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (true) {
|
||||
if (ch == '-') {
|
||||
if (sb.length() > 1) {
|
||||
throw new JsonException("minus inside number");
|
||||
}
|
||||
sb.append((char) ch);
|
||||
ch = reader.read();
|
||||
minus = true;
|
||||
} else if (ch == 'e' || ch == 'E') {
|
||||
sb.append((char) ch);
|
||||
ch = reader.read();
|
||||
if (exponent) {
|
||||
throw new JsonException("double exponents");
|
||||
}
|
||||
exponent = true;
|
||||
ch = reader.read();
|
||||
if (ch == '-' || ch == '+') {
|
||||
ch = reader.read();
|
||||
if (ch < '0' || ch > '9') {
|
||||
throw new JsonException("invalid exponent");
|
||||
}
|
||||
} else if (ch < '0' || ch > '9') {
|
||||
throw new JsonException("invalid exponent");
|
||||
}
|
||||
} else if (ch == '.') {
|
||||
sb.append((char) ch);
|
||||
ch = reader.read();
|
||||
if (dot) {
|
||||
throw new JsonException("multiple dots");
|
||||
}
|
||||
if (sb.length() == 1) {
|
||||
throw new JsonException("no digit before dot");
|
||||
}
|
||||
dot = true;
|
||||
} else if (ch >= '0' && ch <= '9') {
|
||||
sb.append((char) ch);
|
||||
ch = reader.read();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (minus && sb.length() == 1) {
|
||||
throw new JsonException("isolated minus");
|
||||
}
|
||||
if (dot || exponent) {
|
||||
listener.onDouble(Double.parseDouble(sb.toString()));
|
||||
} else {
|
||||
listener.onLong(Long.parseLong(sb.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
private void parseString(boolean isKey) throws IOException, JsonException {
|
||||
reader.mark(1024);
|
||||
ch = reader.read();
|
||||
boolean escaped = false;
|
||||
int count = 1;
|
||||
while (true) {
|
||||
if (ch == '"') {
|
||||
char[] buffer = new char[count - 1];
|
||||
reader.reset();
|
||||
reader.read(buffer, 0, count - 1);
|
||||
reader.read();
|
||||
CharSequence s = new String(buffer);
|
||||
if (escaped) {
|
||||
s = unescape(s);
|
||||
if (isKey) {
|
||||
listener.onKey(s);
|
||||
} else {
|
||||
listener.onValue(s);
|
||||
}
|
||||
} else {
|
||||
if (isKey) {
|
||||
listener.onKey(s);
|
||||
} else {
|
||||
listener.onValue(s);
|
||||
}
|
||||
}
|
||||
ch = reader.read();
|
||||
return;
|
||||
} else if (ch == '\\') {
|
||||
escaped = true;
|
||||
ch = reader.read();
|
||||
if (ch == '"' || ch == '/' || ch == '\\' || ch == 'b' || ch == 'f' || ch == 'n' || ch == 'r' || ch == 't') {
|
||||
ch = reader.read();
|
||||
count += 2;
|
||||
} else if (ch == 'u') {
|
||||
expectHex();
|
||||
expectHex();
|
||||
expectHex();
|
||||
expectHex();
|
||||
count += 5;
|
||||
} else {
|
||||
throw new JsonException("illegal escape char: " + ch);
|
||||
}
|
||||
} else if (ch < 32) {
|
||||
throw new JsonException("illegal control char: " + ch);
|
||||
} else {
|
||||
count++;
|
||||
ch = reader.read();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void parseList() throws IOException {
|
||||
int count = 0;
|
||||
listener.beginCollection();
|
||||
ch = reader.read();
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
if (ch == ']') {
|
||||
listener.endCollection();
|
||||
ch = reader.read();
|
||||
return;
|
||||
}
|
||||
if (count > 0) {
|
||||
expectChar(',');
|
||||
ch = reader.read();
|
||||
skipWhitespace();
|
||||
}
|
||||
parseValue();
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
private void parseMap() throws IOException, JsonException {
|
||||
int count = 0;
|
||||
listener.beginMap();
|
||||
ch = reader.read();
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
if (ch == '}') {
|
||||
listener.endMap();
|
||||
ch = reader.read();
|
||||
return;
|
||||
}
|
||||
if (count > 0) {
|
||||
expectChar(',');
|
||||
ch = reader.read();
|
||||
skipWhitespace();
|
||||
}
|
||||
expectChar('"');
|
||||
parseString(true);
|
||||
skipWhitespace();
|
||||
expectChar(':');
|
||||
ch = reader.read();
|
||||
skipWhitespace();
|
||||
parseValue();
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
private void parseNull() throws IOException, JsonException {
|
||||
ch = reader.read();
|
||||
expectChar('u');
|
||||
ch = reader.read();
|
||||
expectChar('l');
|
||||
ch = reader.read();
|
||||
expectChar('l');
|
||||
listener.onNull();
|
||||
ch = reader.read();
|
||||
}
|
||||
|
||||
private void parseTrue() throws IOException, JsonException {
|
||||
ch = reader.read();
|
||||
expectChar('r');
|
||||
ch = reader.read();
|
||||
expectChar('u');
|
||||
ch = reader.read();
|
||||
expectChar('e');
|
||||
listener.onTrue();
|
||||
ch = reader.read();
|
||||
}
|
||||
|
||||
private void parseFalse() throws IOException, JsonException {
|
||||
ch = reader.read();
|
||||
expectChar('a');
|
||||
ch = reader.read();
|
||||
expectChar('l');
|
||||
ch = reader.read();
|
||||
expectChar('s');
|
||||
ch = reader.read();
|
||||
expectChar('e');
|
||||
listener.onFalse();
|
||||
ch = reader.read();
|
||||
}
|
||||
|
||||
private void expectChar(char expected) throws JsonException {
|
||||
if (ch != expected) {
|
||||
throw new JsonException("expected char " + expected + " but got " + (char)ch);
|
||||
}
|
||||
}
|
||||
|
||||
private void expectHex() throws IOException, JsonException {
|
||||
ch = reader.read();
|
||||
if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')) {
|
||||
return;
|
||||
}
|
||||
throw new JsonException("invalid hex char " + ch);
|
||||
}
|
||||
|
||||
private void skipWhitespace() throws IOException {
|
||||
while (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') {
|
||||
ch = reader.read();
|
||||
}
|
||||
}
|
||||
|
||||
private static CharSequence unescape(CharSequence input) {
|
||||
StringBuilder result = new StringBuilder(input.length());
|
||||
int i = 0;
|
||||
while (i < input.length()) {
|
||||
if (input.charAt(i) == '\\') {
|
||||
i++;
|
||||
switch (input.charAt(i)) {
|
||||
case '\\' -> result.append('\\');
|
||||
case '/' -> result.append('/');
|
||||
case '"' -> result.append('"');
|
||||
case 'b' -> result.append('\b');
|
||||
case 'f' -> result.append('\f');
|
||||
case 'n' -> result.append('\n');
|
||||
case 'r' -> result.append('\r');
|
||||
case 't' -> result.append('\t');
|
||||
case 'u' -> {
|
||||
result.append(Character.toChars(Integer.parseInt(input.toString().substring(i + 1, i + 5), 16)));
|
||||
i += 4;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.append(input.charAt(i));
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
6
src/main/java/org/xbib/marc/json/JsonResultListener.java
Normal file
6
src/main/java/org/xbib/marc/json/JsonResultListener.java
Normal file
|
@ -0,0 +1,6 @@
|
|||
package org.xbib.marc.json;
|
||||
|
||||
public interface JsonResultListener extends JsonListener {
|
||||
|
||||
Node<?> getResult();
|
||||
}
|
24
src/main/java/org/xbib/marc/json/KeyNode.java
Normal file
24
src/main/java/org/xbib/marc/json/KeyNode.java
Normal file
|
@ -0,0 +1,24 @@
|
|||
package org.xbib.marc.json;
|
||||
|
||||
public class KeyNode implements Node<CharSequence> {
|
||||
|
||||
private CharSequence value;
|
||||
|
||||
public KeyNode(CharSequence value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence get() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value != null ? value.toString() : null;
|
||||
}
|
||||
}
|
6
src/main/java/org/xbib/marc/json/ListNode.java
Normal file
6
src/main/java/org/xbib/marc/json/ListNode.java
Normal file
|
@ -0,0 +1,6 @@
|
|||
package org.xbib.marc.json;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ListNode extends Node<List<Node<?>>> {
|
||||
}
|
6
src/main/java/org/xbib/marc/json/MapNode.java
Normal file
6
src/main/java/org/xbib/marc/json/MapNode.java
Normal file
|
@ -0,0 +1,6 @@
|
|||
package org.xbib.marc.json;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface MapNode extends Node<Map<CharSequence, Node<?>>> {
|
||||
}
|
113
src/main/java/org/xbib/marc/json/MarcJsonListener.java
Normal file
113
src/main/java/org/xbib/marc/json/MarcJsonListener.java
Normal file
|
@ -0,0 +1,113 @@
|
|||
package org.xbib.marc.json;
|
||||
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedList;
|
||||
|
||||
public class MarcJsonListener implements JsonResultListener {
|
||||
|
||||
private Node<?> node;
|
||||
|
||||
private final Deque<Node<?>> stack = new LinkedList<>();
|
||||
|
||||
private final ValueNode NULL_NODE = new ValueNode(null);
|
||||
|
||||
private final ValueNode TRUE_NODE = new ValueNode(Boolean.TRUE);
|
||||
|
||||
private final ValueNode FALSE_NODE = new ValueNode(Boolean.FALSE);
|
||||
|
||||
public MarcJsonListener() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node<?> getResult() {
|
||||
return node;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void begin() {
|
||||
stack.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void end() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNull() {
|
||||
valueNode(NULL_NODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrue() {
|
||||
valueNode(TRUE_NODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFalse() {
|
||||
valueNode(FALSE_NODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKey(CharSequence key) {
|
||||
stack.push(new KeyNode(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onValue(CharSequence value) {
|
||||
valueNode(new ValueNode(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLong(Long value) {
|
||||
valueNode(new ValueNode(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDouble(Double value) {
|
||||
valueNode(new ValueNode(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beginCollection() {
|
||||
stack.push(new MarcListNode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endCollection() {
|
||||
node = stack.pop();
|
||||
tryAppend(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beginMap() {
|
||||
stack.push(new MarcMapNode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endMap() {
|
||||
node = stack.pop();
|
||||
tryAppend(node);
|
||||
}
|
||||
|
||||
private void valueNode(ValueNode valueNode) {
|
||||
if (!tryAppend(valueNode)) {
|
||||
stack.push(valueNode);
|
||||
node = valueNode;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean tryAppend(Node<?> node) {
|
||||
if (!stack.isEmpty()) {
|
||||
if (stack.peek() instanceof MarcListNode listNode) {
|
||||
listNode.add(node);
|
||||
return true;
|
||||
} else if (stack.peek() instanceof KeyNode) {
|
||||
KeyNode keyNode = (KeyNode) stack.pop();
|
||||
MarcMapNode mapNode = (MarcMapNode) stack.peek();
|
||||
mapNode.put(keyNode.get(), node);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
15
src/main/java/org/xbib/marc/json/MarcListNode.java
Normal file
15
src/main/java/org/xbib/marc/json/MarcListNode.java
Normal file
|
@ -0,0 +1,15 @@
|
|||
package org.xbib.marc.json;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class MarcListNode extends LinkedList<Node<?>> implements ListNode {
|
||||
|
||||
public MarcListNode() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Node<?>> get() {
|
||||
return this;
|
||||
}
|
||||
}
|
15
src/main/java/org/xbib/marc/json/MarcMapNode.java
Normal file
15
src/main/java/org/xbib/marc/json/MarcMapNode.java
Normal file
|
@ -0,0 +1,15 @@
|
|||
package org.xbib.marc.json;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class MarcMapNode extends LinkedHashMap<CharSequence, Node<?>> implements MapNode {
|
||||
|
||||
public MarcMapNode() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<CharSequence, Node<?>> get() {
|
||||
return this;
|
||||
}
|
||||
}
|
6
src/main/java/org/xbib/marc/json/Node.java
Normal file
6
src/main/java/org/xbib/marc/json/Node.java
Normal file
|
@ -0,0 +1,6 @@
|
|||
package org.xbib.marc.json;
|
||||
|
||||
public interface Node<T> {
|
||||
|
||||
T get();
|
||||
}
|
9
src/main/java/org/xbib/marc/json/Parser.java
Normal file
9
src/main/java/org/xbib/marc/json/Parser.java
Normal file
|
@ -0,0 +1,9 @@
|
|||
package org.xbib.marc.json;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
|
||||
public interface Parser {
|
||||
|
||||
Node<?> parse(Reader reader) throws IOException;
|
||||
}
|
29
src/main/java/org/xbib/marc/json/ValueNode.java
Normal file
29
src/main/java/org/xbib/marc/json/ValueNode.java
Normal file
|
@ -0,0 +1,29 @@
|
|||
package org.xbib.marc.json;
|
||||
|
||||
public class ValueNode implements Node<Object> {
|
||||
|
||||
private Object value;
|
||||
|
||||
public ValueNode(Object value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDepth() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void set(Object value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value != null ? value.toString() : null;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue