/* * #! * Ontopia Engine * #- * Copyright (C) 2001 - 2013 The Ontopia Project * #- * 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.xfml; import java.net.MalformedURLException; import java.util.Collection; import java.util.HashSet; import net.ontopia.infoset.core.LocatorIF; import net.ontopia.infoset.impl.basic.URILocator; import net.ontopia.topicmaps.core.OccurrenceIF; 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.TopicMapStoreFactoryIF; import net.ontopia.topicmaps.core.TopicMapStoreIF; import net.ontopia.topicmaps.impl.utils.AbstractTopicMapStore; import net.ontopia.topicmaps.utils.AssociationBuilder; import net.ontopia.topicmaps.utils.MergeUtils; import net.ontopia.topicmaps.xml.AbstractTopicMapContentHandler; import net.ontopia.utils.OntopiaRuntimeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.Attributes; import org.xml.sax.SAXException; /** * INTERNAL: SAX2 content handler used for importing XFML documents. * The content handler builds a topic map object based on a SAX event * stream conforming to the XFML interchange syntax. */ public class XFMLContentHandler extends AbstractTopicMapContentHandler { static final String NO_URI = "URI"; static final String EL_XFML = "xfml"; static final String EL_FACET = "facet"; static final String EL_TOPIC = "topic"; static final String EL_NAME = "name"; static final String EL_PSI = "psi"; static final String EL_DESCRIPTION = "description"; static final String EL_PAGE = "page"; static final String EL_TITLE = "title"; static final String EL_CONNECT = "connect"; static final String EL_OCCURRENCE = "occurrence"; // Define a logging category. static Logger log = LoggerFactory.getLogger(XFMLContentHandler.class.getName()); protected TopicMapStoreFactoryIF stores; protected LocatorIF map_uri; private TopicMapIF topicmap; private TopicMapBuilderIF builder; private StringBuilder content; private boolean keep_content; // parse state private TopicIF current_topic; // PSI holders private AssociationBuilder parentBuilder; private AssociationBuilder occursBuilder; private TopicIF PSI_DESCRIPTION; public XFMLContentHandler(TopicMapStoreFactoryIF stores, LocatorIF base_address) { super(base_address); this.stores = stores; } public XFMLContentHandler(TopicMapStoreFactoryIF stores, LocatorIF base_address, Collection processed_documents) { super(base_address, processed_documents); this.stores = stores; } /** * INTERNAL: Gets the topic map found after having parsed the input source. */ public TopicMapIF getTopicMap() { return topicmap; } // -------------------------------------------------------------------------- // Document events // -------------------------------------------------------------------------- public void startDocument () { // Initialize variables parents.clear(); info.clear(); keep_content = false; content = new StringBuilder(); topicmap = stores.createStore().getTopicMap(); builder = topicmap.getBuilder(); makePSIs(); log.info("Processing document '" + doc_address + "'."); // Initialize list of accumulated processed documents this.processed_documents_accumulated = new HashSet(); } public void endDocument () { // Copy list of accumulated processed documents to parent list this.processed_documents_from_parent.addAll(processed_documents_accumulated); // log.debug("Stack size: " + parents.size()); // log.debug("Info map: " + info); } public void startElement (String uri, String name, String qName, Attributes atts) throws SAXException { try { //log.debug("S: '" + uri + "' " + qName); // ----- <xfml> ----------------------------------------------------------- if (qName == EL_XFML) { String version = atts.getValue("version"); if (version == null) log.warn("No version attribute on 'xfml' element"); if (!version.equals("1.0")) log.warn("Unsupported XFML version: " + version); String mapurl = atts.getValue("url"); if (mapurl == null) log.warn("No url attribute on 'xfml' element"); else { try { map_uri = new URILocator(mapurl); TopicMapStoreIF store = topicmap.getStore(); if (store instanceof AbstractTopicMapStore && store.getBaseAddress() == null) ((AbstractTopicMapStore) store).setBaseAddress(map_uri); doc_address = map_uri; // Add this document to the list of processed documents. processed_documents_accumulated.add(getBaseAddress()); } catch (MalformedURLException e) { log.warn("Invalid xfml base URL: " + mapurl); } } // FIXME: what to do about language? } // ----- <facet> ---------------------------------------------------------- else if (qName == EL_FACET) { String id = atts.getValue("id"); // FIXME: complain if no id current_topic = builder.makeTopic(); registerSourceLocator(current_topic, id); keep_content = true; } // ----- <topic> ---------------------------------------------------------- else if (qName == EL_TOPIC) { String id = atts.getValue("id"); // FIXME: complain if no id current_topic = builder.makeTopic(); registerSourceLocator(current_topic, id); String parentid = atts.getValue("parentTopicid"); if (parentid == null) parentid = atts.getValue("facetid"); // FIXME: complain if no refs TopicIF parent = resolveTopicRef("#" + parentid); parentBuilder.makeAssociation(parent, current_topic); } // ----- <page> ----------------------------------------------------------- else if (qName == EL_PAGE) { String url = atts.getValue("url"); // FIXME: complain if no url current_topic = builder.makeTopic(); current_topic.addSubjectLocator(createLocator(url)); } // ----- <occurrence>------------------------------------------------------ else if (qName == EL_OCCURRENCE) { String topicid = atts.getValue("topicid"); // FIXME: complain if none TopicIF subject = resolveTopicRef("#" + topicid); occursBuilder.makeAssociation(subject, current_topic); } // ----- <name> ----------------------------------------------------------- // ----- <psi> ------------------------------------------------------------ // ----- <description> ---------------------------------------------------- // ----- <title> ---------------------------------------------------------- // ----- <connect> -------------------------------------------------------- else if (qName == EL_NAME || qName == EL_PSI || qName == EL_DESCRIPTION || qName == EL_TITLE || qName == EL_CONNECT) keep_content = true; } catch (RuntimeException e) { e.printStackTrace(); throw e; } } public void characters (char ch[], int start, int length) { if (keep_content) content.append(ch, start, length); } public void endElement (String uri, String name, String qName) throws SAXException { // log.debug("E: " + qName); // ----- </facet> --------------------------------------------------------- if (qName == EL_FACET) builder.makeTopicName(current_topic, content.toString()); // ----- </name> ---------------------------------------------------------- // ----- </title> --------------------------------------------------------- else if ((qName == EL_NAME || qName == EL_TITLE) && current_topic != null) builder.makeTopicName(current_topic, content.toString()); // ----- </psi> ----------------------------------------------------------- else if (qName == EL_PSI) addSubjectIdentifier(current_topic, createLocator(content.toString())); // ----- </description> --------------------------------------------------- else if (qName == EL_DESCRIPTION) { OccurrenceIF occ = builder.makeOccurrence(current_topic, PSI_DESCRIPTION, content.toString()); } // ----- </connect> ------------------------------------------------------- else if (qName == EL_CONNECT) current_topic.addItemIdentifier(createLocator(content.toString())); keep_content = false; content.setLength(0); } // -------------------------------------------------------------------------- // Misc. methods // -------------------------------------------------------------------------- protected LocatorIF getBaseAddress() { return map_uri; } protected TopicIF resolveTopicRef(String address) throws SAXException { LocatorIF locator = createLocator(address); TopicIF topic = (TopicIF) topicmap.getObjectByItemIdentifier(locator); if (topic == null) topic = topicmap.getTopicBySubjectIdentifier(locator); if (topic == null) { if (address.charAt(0) == '#' || locator.getAddress().startsWith(getBaseAddress().getAddress()+"#")) { // this is a local reference; create a topic and return it topic = builder.makeTopic(); topic.addItemIdentifier(locator); } else throw new OntopiaRuntimeException("INTERNAL: Topic ID must begin with '#'"); } return topic; } protected void registerSourceLocator(TopicIF tmobject, String id) { // No need to register source locator if id is null if (id == null) return; // Create source locator LocatorIF locator = createLocator("#" + id); // Add the source locator addItemIdentifier(tmobject, locator); } protected void registerSourceLocator(TMObjectIF tmobject, String id) { // No need to register source locator if id is null if (id == null) return; tmobject.addItemIdentifier(createLocator("#" + id)); } protected void addItemIdentifier(TopicIF topic, LocatorIF locator) { // Check to see if source locator is a subject indicator of // another topic. If so they should merge. TopicIF other_topic = topicmap.getTopicBySubjectIdentifier(locator); if (other_topic != null) { if (log.isInfoEnabled()) log.info("Topic " + topic + " merged with + " + other_topic + " because the source locator is the same as the subject indicator of the other: " + locator); // Merge topic with other topic. MergeUtils.mergeInto(topic, other_topic); // Subject indicator is no longer needed topic.removeSubjectIdentifier(locator); } // Add source locator to object topic.addItemIdentifier(locator); } protected void addSubjectIdentifier(TopicIF topic, LocatorIF locator) { // Check to see if subject indicator is a source locator of another topic. If so they should merge. TMObjectIF other_topic = topicmap.getObjectByItemIdentifier(locator); if (other_topic != null && other_topic instanceof TopicIF) { if (log.isInfoEnabled()) log.info("Topic " + topic + " merged with + " + other_topic + " because the subject indicator is the same as the source locator of the other: " + locator); // Merge topic with other topic. MergeUtils.mergeInto(topic, (TopicIF)other_topic); // Note: subject indicator was never added (there is no need to). } else { // Add subject indicator to object topic.addSubjectIdentifier(locator); } } protected LocatorIF createLocator(String address) { if (address.length() == 0) return getBaseAddress(); else if (address.charAt(0) == '#') // this is necessary because URI refs of the form "#foo" are // same-document references (RFC 2396 - 4.2), and resolve // relative to the document address, regardless of what base // address may be in effect inside the document return doc_address.resolveAbsolute(address); else return getBaseAddress().resolveAbsolute(address); } protected LocatorIF createURILocator(String address) { try { return new URILocator(address); } catch (MalformedURLException e) { throw new OntopiaRuntimeException(e); } } // --- Internal methods private void makePSIs() { try { PSI_DESCRIPTION = builder.makeTopic(); PSI_DESCRIPTION.addSubjectIdentifier(new URILocator("http://psi.ontopia.net/xtm/occurrence-type/description")); String xfml = "http://psi.ontopia.net/xfml/#"; TopicIF parent_child = makeTopic(xfml + "parent-child"); TopicIF parent = makeTopic(xfml + "parent"); TopicIF child = makeTopic(xfml + "child"); parentBuilder = new AssociationBuilder(parent_child, parent, child); TopicIF occurs_on = makeTopic(xfml + "occurs-on"); TopicIF page = makeTopic(xfml + "page"); TopicIF subject = makeTopic(xfml + "subject"); occursBuilder = new AssociationBuilder(occurs_on, subject, page); } catch (MalformedURLException e) { throw new OntopiaRuntimeException(e); } } private TopicIF makeTopic(String psi) throws MalformedURLException { TopicIF topic = builder.makeTopic(); topic.addSubjectIdentifier(new URILocator(psi)); return topic; } }