//#! Ignore-License
/*
* Copyright 2009 Thomas Neidhart, thomas.neidhart@spaceapplications.com
* based on work from Lars Heuer (heuer[at]semagia.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.ontopia.topicmaps.utils.jtm;
import java.io.IOException;
import java.io.Reader;
import java.util.Collection;
import java.util.LinkedList;
import java.util.ArrayList;
import net.ontopia.infoset.core.LocatorIF;
import net.ontopia.topicmaps.core.AssociationIF;
import net.ontopia.topicmaps.core.AssociationRoleIF;
import net.ontopia.topicmaps.core.OccurrenceIF;
import net.ontopia.topicmaps.core.ReifiableIF;
import net.ontopia.topicmaps.core.ScopedIF;
import net.ontopia.topicmaps.core.TMObjectIF;
import net.ontopia.topicmaps.core.TopicIF;
import net.ontopia.topicmaps.core.TopicMapBuilderIF;
import net.ontopia.topicmaps.core.TopicMapIF;
import net.ontopia.topicmaps.core.TopicNameIF;
import net.ontopia.topicmaps.core.VariantNameIF;
import net.ontopia.topicmaps.utils.MergeUtils;
import net.ontopia.topicmaps.utils.PSI;
/**
* INTERNAL: A streaming JTM parser.
*
* The JTM parser supports the top level item types "topicmap", "topic",
* "association", "occurrence" and "name". Detached roles and variants are not
* supported.
*/
final class JTMStreamingParser {
/**
* Supported version of the JTM notation is currently "1.0"
*/
public final static String VERSION = "1.0";
private TopicMapIF tm;
private TopicMapBuilderIF builder;
private LocatorIF baseURI;
/**
* Create a new parser that will store the results into the given topic map.
*
* @param topicmap the topic map to import into.
*/
public JTMStreamingParser(TopicMapIF topicmap) {
this.tm = topicmap;
this.builder = tm.getBuilder();
this.baseURI = topicmap.getStore().getBaseAddress();
}
/**
* INTERNAL: Parses a topic map in JTM 1.0 notation from the input reader.
*
* @param reader the input to read the topic map from.
* @throws IOException if some error occurs while reading from the input.
* @throws JTMException if the topic map to be parsed is not in JTM 1.0
* syntax.
*/
public void parse(final Reader reader) throws IOException, JTMException {
JSONParser parser = new JSONParser(reader);
if (parser.nextToken() != JSONToken.START_OBJECT) {
throw new JTMException("Expected input to start with an object: '"
+ JSONToken.nameOf(JSONToken.START_OBJECT) + "'.");
}
if (parser.nextToken() != JSONToken.KW_VERSION) {
throw new JTMException("Expected 'version' at the beginning.");
}
parser.nextToken();
if (!VERSION.equals(parser.getText())) {
throw new JTMException("Unsupported version: '" + parser.getText() + "'.");
}
if (parser.nextToken() != JSONToken.KW_ITEM_TYPE) {
throw new JTMException("Expected 'item_type' after the version.");
}
handleItemType(parser);
}
/**
* INTERNAL: Based on the current item type of the input, call the appropriate
* handle method.
*/
private void handleItemType(final JSONParser parser) throws IOException,
JTMException {
parser.nextToken();
// The item type is case-insensitive; force lower case.
final String itemType = parser.getText().toLowerCase();
if ("topicmap".equals(itemType)) {
handleTopicMap(parser);
} else if ("topic".equals(itemType)) {
handleTopic(parser);
} else if ("association".equals(itemType)) {
handleAssociation(parser);
} else if ("occurrence".equals(itemType)) {
handleOccurrence(parser, null);
} else if ("name".equals(itemType)) {
handleName(parser, null);
} else if ("role".equals(itemType)) {
throw new JTMException("Detached roles are not supported.");
} else if ("variant".equals(itemType)) {
throw new JTMException("Detached variants are not supported.");
} else {
throw new JTMException("Unknown item type: " + itemType);
}
}
/**
* INTERNAL: Handle jtm object of type topic map.
*/
private void handleTopicMap(final JSONParser parser) throws IOException,
JTMException {
while (parser.nextToken() != JSONToken.END_OBJECT) {
switch (parser.getCurrentToken()) {
case JSONToken.KW_IIDS:
Collection<LocatorIF> iids = handleItemIdentifiers(parser);
if (iids != null) {
for (LocatorIF iid : iids) {
TMObjectIF obj = tm.getObjectByItemIdentifier(iid);
if (obj != null) {
throw new JTMException("Item Identifier for topic map already "
+ "used by another construct.");
} else {
tm.addItemIdentifier(iid);
}
}
}
break;
case JSONToken.KW_REIFIER:
setReifier(tm, handleReifier(parser));
break;
case JSONToken.KW_TOPICS:
if (parser.nextToken() == JSONToken.START_ARRAY) {
while (parser.nextToken() != JSONToken.END_ARRAY) {
handleTopic(parser);
}
break;
}
case JSONToken.KW_ASSOCIATIONS:
if (parser.nextToken() == JSONToken.START_ARRAY) {
while (parser.nextToken() != JSONToken.END_ARRAY) {
handleAssociation(parser);
}
break;
}
default:
reportIllegalField(parser);
}
}
}
/**
* INTERNAL: Handle jtm object of type topic.
*/
private void handleTopic(final JSONParser parser) throws IOException,
JTMException {
boolean seenIdentity = false;
TopicIF topic = tm.getBuilder().makeTopic();
while (parser.nextToken() != JSONToken.END_OBJECT) {
switch (parser.getCurrentToken()) {
case JSONToken.KW_SIDS:
if (parser.nextToken() == JSONToken.START_ARRAY) {
while (parser.nextToken() != JSONToken.END_ARRAY) {
LocatorIF sid = resolveIRI(parser.getText());
TopicIF existingTopic = tm.getTopicBySubjectIdentifier(sid);
if (existingTopic != null) {
if (existingTopic != topic) {
MergeUtils.mergeInto(topic, existingTopic);
}
} else {
topic.addSubjectIdentifier(sid);
}
seenIdentity = true;
}
break;
}
case JSONToken.KW_SLOS:
if (parser.nextToken() == JSONToken.START_ARRAY) {
while (parser.nextToken() != JSONToken.END_ARRAY) {
LocatorIF slo = resolveIRI(parser.getText());
TopicIF existingTopic = tm.getTopicBySubjectLocator(slo);
if (existingTopic != null) {
if (topic != existingTopic) {
MergeUtils.mergeInto(topic, existingTopic);
}
} else {
topic.addSubjectLocator(slo);
}
seenIdentity = true;
}
break;
}
case JSONToken.KW_IIDS:
if (parser.nextToken() == JSONToken.START_ARRAY) {
while (parser.nextToken() != JSONToken.END_ARRAY) {
LocatorIF iid = resolveIRI(parser.getText());
TopicIF existingTopic = (TopicIF) tm.getObjectByItemIdentifier(iid);
if (existingTopic != null) {
if (topic != existingTopic) {
MergeUtils.mergeInto(topic, existingTopic);
}
} else {
topic.addItemIdentifier(iid);
}
seenIdentity = true;
}
break;
}
case JSONToken.KW_OCCURRENCES:
if (parser.nextToken() == JSONToken.START_ARRAY) {
if (!seenIdentity) {
throw new JTMException(
"Cannot process occurrences without a previously read identity");
}
while (parser.nextToken() != JSONToken.END_ARRAY) {
handleOccurrence(parser, topic);
}
break;
}
case JSONToken.KW_NAMES:
if (parser.nextToken() == JSONToken.START_ARRAY) {
if (!seenIdentity) {
throw new JTMException(
"Cannot process names without a previously read identity");
}
while (parser.nextToken() != JSONToken.END_ARRAY) {
handleName(parser, topic);
}
break;
}
default:
reportIllegalField(parser);
}
}
if (!seenIdentity) {
throw new JTMException("The topic has no identity.");
}
}
/**
* INTERNAL: Handle jtm object of type occurrence.
*/
private void handleOccurrence(final JSONParser parser, TopicIF topic)
throws IOException, JTMException {
boolean seenType = false;
LocatorIF datatype = PSI.getXSDString();
String value = null;
TopicIF type = null;
Collection<LocatorIF> iids = null;
Collection<TopicIF> scopes = null;
TopicIF reifier = null;
TopicIF parent = null;
while (parser.nextToken() != JSONToken.END_OBJECT) {
switch (parser.getCurrentToken()) {
case JSONToken.KW_TYPE:
if (!seenType) {
parser.nextToken();
type = makeTopicRef(parser.getText());
seenType = true;
break;
}
case JSONToken.KW_VALUE:
if (value == null) {
parser.nextToken();
value = parser.getText();
break;
}
case JSONToken.KW_DATATYPE:
parser.nextToken();
datatype = resolveIRI(parser.getText());
break;
case JSONToken.KW_IIDS:
iids = handleItemIdentifiers(parser);
break;
case JSONToken.KW_REIFIER:
reifier = handleReifier(parser);
break;
case JSONToken.KW_SCOPE:
scopes = handleScope(parser);
break;
case JSONToken.KW_PARENT:
parent = getParentTopic(parser);
break;
default:
reportIllegalField(parser);
}
}
if (value == null) {
throw new JTMException("The value of the occurrence is undefined.");
}
if (!seenType) {
throw new JTMException("The type of the occurrence is undefined.");
}
if (topic == null) {
if (parent == null) {
throw new JTMException("The parent of the occurrence is undefined.");
} else {
topic = parent;
}
}
OccurrenceIF oc;
if (datatype.equals(PSI.getXSDURI())) {
oc = tm.getBuilder().makeOccurrence(topic, type, resolveIRI(value));
} else {
oc = tm.getBuilder().makeOccurrence(topic, type, value, datatype);
}
setScopes(oc, scopes);
setReifier(oc, reifier);
setItemIdentifiers(oc, iids);
}
/**
* INTERNAL: Handle jtm object of type name.
*/
private void handleName(final JSONParser parser, TopicIF topic)
throws IOException, JTMException {
boolean seenValue = false;
boolean seenType = false;
boolean seenParent = false;
boolean requireParent = false;
TopicIF type = null;
String value = null;
Collection<LocatorIF> iids = null;
Collection<TopicIF> scopes = null;
TopicIF reifier = null;
TopicIF parent = null;
Collection<Variant> variants = new ArrayList<Variant>();
// Create an empty name object.
if (topic == null) {
topic = tm.getBuilder().makeTopic();
requireParent = true;
}
while (parser.nextToken() != JSONToken.END_OBJECT) {
switch (parser.getCurrentToken()) {
case JSONToken.KW_TYPE:
if (!seenType) {
parser.nextToken();
type = makeTopicRef(parser.getText());
seenType = true;
break;
}
case JSONToken.KW_VALUE:
if (!seenValue) {
parser.nextToken();
value = parser.getText();
seenValue = true;
break;
}
case JSONToken.KW_IIDS:
iids = handleItemIdentifiers(parser);
break;
case JSONToken.KW_REIFIER:
reifier = handleReifier(parser);
break;
case JSONToken.KW_SCOPE:
scopes = handleScope(parser);
break;
case JSONToken.KW_VARIANTS:
if (parser.nextToken() == JSONToken.START_ARRAY) {
while (parser.nextToken() != JSONToken.END_ARRAY) {
variants.add(handleVariant(parser));
}
break;
}
case JSONToken.KW_PARENT:
parent = getParentTopic(parser);
seenParent = (parent != null) ? true : false;
break;
default:
reportIllegalField(parser);
}
}
if (!seenValue) {
throw new JTMException("The value of the name is undefined");
}
if (!seenType) {
type = makeTopicRef("si:" + PSI.SAM_NAMETYPE);
}
if (requireParent) {
if (!seenParent) {
throw new JTMException("The parent of the name is undefined.");
} else {
MergeUtils.mergeInto(topic, parent);
}
}
TopicNameIF name = tm.getBuilder().makeTopicName(topic, type, value);
setScopes(name, scopes);
setReifier(name, reifier);
setItemIdentifiers(name, iids);
// create the variants
for (Variant tmp : variants) {
VariantNameIF variant;
if (tmp.datatype.equals(PSI.getXSDURI())) {
variant = tm.getBuilder()
.makeVariantName(name, resolveIRI(tmp.value), tmp.scope);
} else {
variant = tm.getBuilder().makeVariantName(name, tmp.value,
tmp.datatype, tmp.scope);
}
setReifier(variant, tmp.reifier);
setItemIdentifiers(variant, tmp.iids);
}
}
/**
* INTERNAL: Handle jtm object of type variant.
*/
private Variant handleVariant(final JSONParser parser)
throws IOException, JTMException {
boolean seenScope = false;
Variant variant = new Variant();
while (parser.nextToken() != JSONToken.END_OBJECT) {
switch (parser.getCurrentToken()) {
case JSONToken.KW_VALUE:
if (variant.value == null) {
parser.nextToken();
variant.value = parser.getText();
break;
}
case JSONToken.KW_DATATYPE:
parser.nextToken();
variant.datatype = resolveIRI(parser.getText());
break;
case JSONToken.KW_IIDS:
variant.iids = handleItemIdentifiers(parser);
break;
case JSONToken.KW_REIFIER:
variant.reifier = handleReifier(parser);
break;
case JSONToken.KW_SCOPE:
if (!seenScope) {
variant.scope = handleScope(parser);
seenScope = true;
break;
}
default:
reportIllegalField(parser);
}
}
if (!seenScope) {
throw new JTMException("The scope of the variant is undefined.");
}
if (variant.value == null) {
throw new JTMException("The value of the variant is undefined.");
}
return variant;
}
/**
* INTERNAL: Handle jtm object of type association.
*/
private void handleAssociation(final JSONParser parser) throws IOException,
JTMException {
boolean seenType = false;
boolean seenRoles = false;
TopicIF type = null;
Collection<LocatorIF> iids = null;
Collection<TopicIF> scopes = null;
TopicIF reifier = null;
// create an empty type in advance, the real type will be set later
TopicIF emptyType = tm.getBuilder().makeTopic();
AssociationIF assoc = tm.getBuilder().makeAssociation(emptyType);
while (parser.nextToken() != JSONToken.END_OBJECT) {
switch (parser.getCurrentToken()) {
case JSONToken.KW_TYPE:
if (!seenType) {
parser.nextToken();
type = makeTopicRef(parser.getText());
seenType = true;
break;
}
case JSONToken.KW_IIDS:
iids = handleItemIdentifiers(parser);
break;
case JSONToken.KW_REIFIER:
reifier = handleReifier(parser);
break;
case JSONToken.KW_SCOPE:
scopes = handleScope(parser);
break;
case JSONToken.KW_ROLES:
if (!seenRoles && parser.nextToken() == JSONToken.START_ARRAY) {
while (parser.nextToken() != JSONToken.END_ARRAY) {
handleRole(parser, assoc);
}
seenRoles = true;
break;
}
default:
reportIllegalField(parser);
}
}
if (!seenType) {
throw new JTMException("The type of the association is undefined");
}
if (!seenRoles) {
throw new JTMException("The association has no roles");
}
assoc.setType(type);
// remove the temporarily created type
emptyType.remove();
setScopes(assoc, scopes);
setReifier(assoc, reifier);
setItemIdentifiers(assoc, iids);
}
/**
* INTERNAL: Handle jtm object of type role.
*/
private void handleRole(final JSONParser parser, AssociationIF assoc)
throws IOException, JTMException {
boolean seenType = false;
boolean seenPlayer = false;
TopicIF type = null;
TopicIF player = null;
Collection<LocatorIF> iids = null;
TopicIF reifier = null;
while (parser.nextToken() != JSONToken.END_OBJECT) {
switch (parser.getCurrentToken()) {
case JSONToken.KW_TYPE:
if (!seenType) {
parser.nextToken();
type = makeTopicRef(parser.getText());
seenType = true;
break;
}
case JSONToken.KW_PLAYER:
if (!seenPlayer) {
parser.nextToken();
player = makeTopicRef(parser.getText());
seenPlayer = true;
break;
}
case JSONToken.KW_IIDS:
iids = handleItemIdentifiers(parser);
break;
case JSONToken.KW_REIFIER:
reifier = handleReifier(parser);
break;
case JSONToken.KW_PARENT:
// ignore parent key, in case it is present.
break;
default:
reportIllegalField(parser);
}
}
if (!seenType) {
throw new JTMException("The type of the role is undefined.");
}
if (!seenPlayer) {
throw new JTMException("The player of the role is undefined.");
}
AssociationRoleIF role = tm.getBuilder().makeAssociationRole(assoc, type,
player);
setReifier(role, reifier);
setItemIdentifiers(role, iids);
}
/**
* INTERNAL: Returns a {@link TopicIF} object that is referenced bu a JTM
* reference string.
*
* @param tid A string which starts with 'si:', 'sl:' or 'ii:' followed by an
* IRI reference.
*/
private TopicIF makeTopicRef(final String tid) throws JTMException {
char[] chars = tid.toCharArray();
if (chars.length > 3 && chars[2] == ':') {
LocatorIF iri = resolveIRI(new String(chars, 3, chars.length - 3));
if (chars[0] == 's') {
if (chars[1] == 'i') {
TopicIF topic = tm.getTopicBySubjectIdentifier(iri);
if (topic == null) {
topic = builder.makeTopic();
topic.addSubjectIdentifier(iri);
}
return topic;
} else if (chars[1] == 'l') {
TopicIF topic = tm.getTopicBySubjectLocator(iri);
if (topic == null) {
topic = builder.makeTopic();
topic.addSubjectLocator(iri);
}
return topic;
}
} else if (chars[0] == 'i' && chars[1] == 'i') {
TopicIF topic = (TopicIF) tm.getObjectByItemIdentifier(iri);
if (topic == null) {
topic = builder.makeTopic();
topic.addItemIdentifier(iri);
}
return topic;
}
}
throw new JTMException("Unknown topic reference: " + tid);
}
/**
* INTERNAL: Returns a {@link LocatorIF} of the given iri, that is resolved to
* the base IRI of the topic map.
*/
private LocatorIF resolveIRI(final String iri) {
return baseURI.resolveAbsolute(iri);
}
/**
* INTERNAL: Returns the reifier iff it is not <tt>null</tt>.
*/
private TopicIF handleReifier(final JSONParser parser) throws IOException,
JTMException {
TopicIF reifier = null;
if (parser.nextToken() != JSONToken.VALUE_NULL) {
reifier = makeTopicRef(parser.getText());
}
return reifier;
}
/**
* INTERNAL: Returns the parent topic for detached jtm objects (e.g. name,
* occurrence).
*/
private TopicIF getParentTopic(final JSONParser parser) throws IOException,
JTMException {
if (parser.nextToken() != JSONToken.START_ARRAY) {
throw new JTMException("Expected an array for the parent value.");
}
TopicIF topic = null;
// iterate over all elements of the array, and merge the resulting topics
// together in a single one.
while (parser.nextToken() != JSONToken.END_ARRAY) {
TopicIF tmp = makeTopicRef(parser.getText());
if (topic == null) {
topic = tmp;
} else {
MergeUtils.mergeInto(topic, tmp);
}
}
return topic;
}
/**
* INTERNAL: Returns a collection of item identifiers that are available for
* the current jtm object.
*/
private Collection<LocatorIF> handleItemIdentifiers(final JSONParser parser)
throws IOException, JTMException {
if (parser.nextToken() != JSONToken.START_ARRAY) {
throw new JTMException("Expected an array for the item identifiers");
}
Collection<LocatorIF> iids = new LinkedList<LocatorIF>();
while (parser.nextToken() != JSONToken.END_ARRAY) {
iids.add(resolveIRI(parser.getText()));
}
return iids;
}
/**
* INTERNAL: Returns all the themes that are associated with the current jtm
* object. The object itself has to check whether it allows scopes or not.
*/
private Collection<TopicIF> handleScope(final JSONParser parser)
throws IOException, JTMException {
if (parser.nextToken() != JSONToken.START_ARRAY) {
throw new JTMException("Expected an array for the scope themes.");
}
Collection<TopicIF> scopes = new LinkedList<TopicIF>();
while (parser.nextToken() != JSONToken.END_ARRAY) {
scopes.add(makeTopicRef(parser.getText()));
}
return scopes;
}
/**
* INTERNAL: Set the reifier for a reifiable topic map construct. If the
* reifier already reifies another construct, ignore it. Duplicate constructs
* will be handled later.
*/
private void setReifier(ReifiableIF object, TopicIF reifier)
throws JTMException {
if (reifier != null) {
if (reifier.getReified() == null) {
object.setReifier(reifier);
} else {
// TMObjectIF other = reifier.getReified();
// // if they are of the same class, try to merge them
// if (object.getClass().isAssignableFrom(other.getClass())) {
// MergeUtils.mergeInto(object, (ReifiableIF) other);
// }
}
}
}
/**
* INTERNAL: Assigns the collection of item identifiers to a topic map
* construct. This method checks whether an item identifier is already in use.
*/
private void setItemIdentifiers(TMObjectIF object, Collection<LocatorIF> iids) {
if (iids != null) {
for (LocatorIF iid : iids) {
TMObjectIF obj = tm.getObjectByItemIdentifier(iid);
if (obj != null) {
} else {
object.addItemIdentifier(iid);
}
}
}
}
/**
* INTERNAL: Assign the given scopes to the specified scoped topic map
* construct.
*/
private void setScopes(ScopedIF object, Collection<TopicIF> scopes) {
if (scopes != null) {
for (TopicIF scope : scopes) {
object.addTheme(scope);
}
}
}
private void reportIllegalField(final JSONParser parser) throws IOException,
JTMException {
throw new JTMException("Unknown key name: '" + parser.getText()
+ "' current: " + JSONToken.nameOf(parser.getCurrentToken()));
}
// --- Temporary variant data holder
// this is necessary to hold the variant data until the topic name
// is created. we can't create the topic name until after the entire
// topic has been read, since otherwise we wind up creating the
// default topic name type, which may not be needed.
private static class Variant {
private LocatorIF datatype;
private String value;
private Collection<LocatorIF> iids;
private Collection<TopicIF> scope;
private TopicIF reifier;
private Variant() {
this.datatype = PSI.getXSDString();
}
}
}