/* * #! * Ontopoly Editor * #- * 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 ontopoly.model; import java.io.IOException; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import net.ontopia.infoset.core.LocatorIF; import net.ontopia.infoset.impl.basic.URILocator; import net.ontopia.topicmaps.core.OccurrenceIF; 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.entry.TopicMapReferenceIF; import net.ontopia.topicmaps.query.core.DeclarationContextIF; import net.ontopia.topicmaps.query.core.InvalidQueryException; import net.ontopia.topicmaps.query.core.QueryProcessorIF; import net.ontopia.topicmaps.query.utils.QueryUtils; import net.ontopia.topicmaps.utils.MergeUtils; import net.ontopia.topicmaps.utils.TopicStringifiers; import net.ontopia.topicmaps.xml.XTMTopicMapReference; import net.ontopia.utils.CollectionUtils; import net.ontopia.utils.OntopiaRuntimeException; import ontopoly.utils.OntopolyModelUtils; /** * INTERNAL: Represents an Ontopoly topic map. */ public class TopicMap { static final String ON = "http://psi.ontopia.net/ontology/"; static final String XTM = "http://www.topicmaps.org/xtm/1.0/core.xtm#"; static final String TEST = "http://psi.example.org/test/"; static final String TECH = "http://www.techquila.com/psi/hierarchy/#"; static final String DC = "http://purl.org/dc/elements/1.1/"; static final String XSD = "http://www.w3.org/2001/XMLSchema#"; static final String TMDM = "http://psi.topicmaps.org/iso13250/model/"; private static final String declarations = "using xtm for i\"" + XTM + "\" " + "using on for i\"" + ON + "\" " + "using test for i\"" + TEST + "\" " + "using tech for i\"" + TECH + "\" " + "using tmdm for i\"" + TMDM + "\" " + "using dc for i\"" + DC + "\" "; private TopicMapIF topicMapIF; private DeclarationContextIF dc; private String topicMapId; private QueryProcessorIF qp; private TopicIF defnametype; // cached here to avoid constant lookups public TopicMap(TopicMapIF topicMapIF, String topicMapId) { this.topicMapIF = topicMapIF; this.topicMapId = topicMapId; // initialize query wrapper initQueryContext(); } private void initQueryContext() { try { this.qp = QueryUtils.getQueryProcessor(topicMapIF); // load built-in declarations this.dc = QueryUtils.parseDeclarations(topicMapIF, declarations); // load custom declarations TopicIF typeIf = OntopolyModelUtils.getTopicIF(this, PSI.ON_TOLOG_DECLARATIONS, false); if (typeIf != null) { TopicIF topicIf = getTopicMapIF().getReifier(); if (topicIf != null) { OccurrenceIF occ = OntopolyModelUtils.findOccurrence(typeIf, topicIf); if (occ != null) this.dc = QueryUtils.parseDeclarations(topicMapIF, declarations, this.dc); } } } catch (InvalidQueryException e) { throw new OntopiaRuntimeException(e); } } public DeclarationContextIF getDeclarationContext() { return dc; } public QueryProcessorIF getQueryProcessor() { return qp; } public boolean containsOntology() { return (getTopicMapIF().getTopicBySubjectIdentifier(PSI.ON_ONTOLOGY_VERSION) != null); } public boolean isDeleteable() { return getTopicMapIF().getStore().getReference().getSource() .supportsDelete(); } public TopicMapIF getTopicMapIF() { return topicMapIF; } public <T> QueryMapper<T> newQueryMapperNoWrap() { return new QueryMapper<T>(getQueryProcessor(), getDeclarationContext()); } public <T> QueryMapper<T> newQueryMapper(final Class<T> type) { return new QueryMapper<T>(getQueryProcessor(), getDeclarationContext()) { @SuppressWarnings("unchecked") @Override protected T wrapValue(Object value) { // don't wrap if type is null if (type == null) return (T)value; // if (value == null) return null; try { Constructor<T> constructor = getConstructor(); if (constructor == null) { throw new OntopolyModelRuntimeException("Couldn't find constructor for the class: " + type); } return constructor.newInstance(new Object[] { value, TopicMap.this }); } catch (Exception e) { throw new OntopolyModelRuntimeException(e); } } private Constructor<T> getConstructor() throws SecurityException, NoSuchMethodException { return type.getConstructor(TopicIF.class, TopicMap.class); } }; } public TopicIF getTopicIFById(String id) { // look up topic by object id or subject identifier TopicIF topic = (TopicIF)getTopicMapIF().getObjectById(id); if (topic == null) { try { return topicMapIF.getTopicBySubjectIdentifier(URILocator.create(id)); } catch (Exception e) { return null; } } return topic; } public Topic getTopicById(String id) { TopicIF topic = getTopicIFById(id); return topic == null ? null : new Topic(topic, this); } public TopicType getTopicTypeById(String id) { TopicIF topic = getTopicIFById(id); return topic == null ? null : new TopicType(topic, this); } public Topic getReifier() { return new Topic(makeReifier(), this); } protected TopicIF makeReifier() { TopicIF reifier = getTopicMapIF().getReifier(); if (reifier == null) { // IMPORTANT: check old-style reification TopicMapIF tm = getTopicMapIF(); Iterator<LocatorIF> iter = tm.getItemIdentifiers().iterator(); while (iter.hasNext()) { LocatorIF srcloc = iter.next(); TopicIF _reifier = tm.getTopicBySubjectIdentifier(srcloc); if (_reifier != null) { if (reifier != null) MergeUtils.mergeInto(reifier, _reifier); else reifier = _reifier; } } if (reifier == null) reifier = tm.getBuilder().makeTopic(); tm.setReifier(reifier); reifier.addType(tm.getTopicBySubjectIdentifier(PSI.ON_TOPIC_MAP)); } return reifier; } /** * Returns the name of the topic map, or null if it has none. */ public String getName() { TopicIF reifier = getTopicMapIF().getReifier(); return reifier == null ? null : TopicStringifiers.toString(reifier); } /** * Returns the version of the Ontopoly meta-ontology used in this topic map. */ public float getOntologyVersion() { TopicIF reifier = getTopicMapIF().getReifier(); if (reifier == null) { // this is where issue 10 kicks in: if two topic maps have been // merged there is no reifier for the topic map any more. however, // the reifier will still be there, as a topic type type "topic map". // so we find that topic by simply querying for it, and then move on // from there. QueryMapper<TopicIF> qm = newQueryMapperNoWrap(); reifier = qm.queryForObject("instance-of($T, on:topic-map)?"); } TopicIF ontologyVersion = getTopicMapIF().getTopicBySubjectIdentifier(PSI.ON_ONTOLOGY_VERSION); Collection<TopicIF> scope = Collections.emptySet(); Collection<OccurrenceIF> occs = OntopolyModelUtils.findOccurrences(ontologyVersion, reifier, scope); if (occs.isEmpty()) return 0; String versionNumber = occs.iterator().next().getValue(); try { return Float.parseFloat(versionNumber); } catch (NumberFormatException e) { return 0; } } /** * Returns the Id of the topic map reference in the topic map registry. */ public String getId() { return topicMapId; } /** * Returns a list of the topic types that is not a system topic type. */ public List<TopicType> getTopicTypes() { String query = "select $type from instance-of($type, on:topic-type) order by $type?"; QueryMapper<TopicType> qm = newQueryMapper(TopicType.class); return qm.queryForList(query); } /** * Returns a list of the topic types that is not a system topic type. */ public List<TopicType> getRootTopicTypes() { String query = "select $type from instance-of($type, on:topic-type), " + "not(xtm:superclass-subclass($type : xtm:subclass, $SUP : xtm:superclass)) " + "order by $type?"; QueryMapper<TopicType> qm = newQueryMapper(TopicType.class); return qm.queryForList(query); } /** * Returns a list of the topic types that is not a system topic type. */ public List<TopicType> getTopicTypesWithLargeInstanceSets() { String query = "select $type from on:has-large-instance-set($type : on:topic-type)?"; QueryMapper<TopicType> qm = newQueryMapper(TopicType.class); return qm.queryForList(query); } // public List<OccurrenceType> getOccurrenceTypes() { // String query = "select $type from " // + "instance-of($type, on:occurrence-type)" // + " order by $type?"; // // QueryMapper<OccurrenceType> qm = newQueryMapper(OccurrenceType.class); // return qm.queryForList(query); // } public List<OccurrenceField> getOccurrenceFields() { String query = "select $field from direct-instance-of($field, on:occurrence-field) order by $field?"; QueryMapper<OccurrenceField> qm = newQueryMapper(OccurrenceField.class); return qm.queryForList(query); } // public List<AssociationType> getAssociationTypes() { // String query = "select $type from " // + "instance-of($type, on:association-type)" // + " order by $type?"; // // QueryMapper<AssociationType> qm = newQueryMapper(AssociationType.class); // return qm.queryForList(query); // } // public List<RoleType> getRoleTypes(boolean includeSystemTopics) { // String query = ""; // if (includeSystemTopics) // query = "select $type from direct-instance-of($type, on:role-type) order by $type?"; // else // query = "select $type from " // + "direct-instance-of($type, on:role-type)" // + ", not(instance-of($type, on:system-topic))" // + " order by $type?"; // // QueryMapper<RoleType> qm = newQueryMapper(RoleType.class); // return qm.queryForList(query); // } public List<RoleField> getRoleFields() { String query = "select $field from direct-instance-of($field, on:role-field) order by $field?"; QueryMapper<RoleField> qm = newQueryMapper(RoleField.class); return qm.queryForList(query); } // public List<NameType> getNameTypes() { // String query = "select $type from direct-instance-of($type, on:name-type) order by $type?"; // // QueryMapper<NameType> qm = newQueryMapper(NameType.class); // return qm.queryForList(query); // } public List<NameField> getNameFields() { String query = "select $field from direct-instance-of($field, on:name-field) order by $field?"; QueryMapper<NameField> qm = newQueryMapper(NameField.class); return qm.queryForList(query); } // public List<IdentityType> getIdentityTypes() { // String query = "select $type from instance-of($type, on:identity-type) order by $type?"; // // QueryMapper<IdentityType> qm = newQueryMapper(IdentityType.class); // return qm.queryForList(query); // } public List<IdentityField> getIdentityFields() { String query = "select $field from instance-of($field, on:identity-field) order by $field?"; QueryMapper<IdentityField> qm = newQueryMapper(IdentityField.class); return qm.queryForList(query); } public List<QueryField> getQueryFields() { String query = "select $field from instance-of($field, on:query-field) order by $field?"; QueryMapper<QueryField> qm = newQueryMapper(QueryField.class); return qm.queryForList(query); } public boolean isSaveable() { TopicMapReferenceIF ref = getTopicMapIF().getStore().getReference(); return (ref instanceof XTMTopicMapReference); } public void save() { TopicMapReferenceIF ref = getTopicMapIF().getStore().getReference(); if (ref instanceof XTMTopicMapReference) { try { ((XTMTopicMapReference) ref).save(); } catch (IOException e) { throw new OntopiaRuntimeException(e); } } } TopicIF createNamedTopic(String name, TopicIF type) { TopicMapBuilderIF builder = getTopicMapIF().getBuilder(); TopicIF topic = builder.makeTopic(type); if (name != null && !name.equals("")) builder.makeTopicName(topic, name); return topic; } public TopicType createTopicType(String name) { TopicIF topicTypeIf= OntopolyModelUtils.getTopicIF(this, PSI.ON, "topic-type"); TopicIF topicIf = createNamedTopic(name, topicTypeIf); TopicType topicType = new TopicType(topicIf, this); topicType.addField(getDefaultNameField()); return topicType; } protected NameField getDefaultNameField() { TopicMap tm = this; NameType nameType = new NameType(OntopolyModelUtils.getTopicIF(tm, PSI.TMDM_TOPIC_NAME), tm); Collection<NameField> nameFields = nameType.getDeclaredByFields(); return CollectionUtils.getFirstElement(nameFields); } // public IdentityField getIdentityField(IdentityType identityType) { // String query = "select $FD from on:has-identity-type(%type% : on:identity-type, $FD : on:identity-field) limit 1?"; // Map<String,TopicIF> params = Collections.singletonMap("type", identityType.getTopicIF()); // // QueryMapper<TopicIF> qm = newQueryMapperNoWrap(); // TopicIF fieldTopic = qm.queryForObject(query, params); // if (fieldTopic == null) // throw new OntopolyModelRuntimeException("Could not find identity field for " + identityType); // // return new IdentityField(fieldTopic, this, identityType); // } public NameType createNameType(String name) { TopicMap tm = this; TopicMapBuilderIF builder = tm.getTopicMapIF().getBuilder(); // create name type TopicIF nameTypeTopic = createNamedTopic(name, OntopolyModelUtils.getTopicIF(tm, PSI.ON_NAME_TYPE)); NameType nameType = new NameType(nameTypeTopic, tm); // create name field TopicIF nameFieldType = OntopolyModelUtils.getTopicIF(tm, PSI.ON, "name-field"); TopicIF nameFieldTopic = builder.makeTopic(nameFieldType); // on:has-name-type($TT : on:name-type, $FD : on:name-field) final TopicIF HAS_NAME_TYPE = OntopolyModelUtils.getTopicIF(tm, PSI.ON, "has-name-type"); final TopicIF NAME_TYPE = OntopolyModelUtils.getTopicIF(tm, PSI.ON, "name-type"); final TopicIF NAME_FIELD = OntopolyModelUtils.getTopicIF(tm, PSI.ON, "name-field"); OntopolyModelUtils.makeBinaryAssociation(HAS_NAME_TYPE, nameTypeTopic, NAME_TYPE, nameFieldTopic, NAME_FIELD); return nameType; } // public NameField getNameField(NameType nameType) { // String query = "select $FD from on:has-name-type(%type% : on:name-type, $FD : on:name-field) limit 1?"; // Map<String,TopicIF> params = Collections.singletonMap("type", nameType.getTopicIF()); // // QueryMapper<TopicIF> qm = newQueryMapperNoWrap(); // TopicIF fieldTopic = qm.queryForObject(query, params); // if (fieldTopic == null) // throw new OntopolyModelRuntimeException("Could not find name field for " + nameType); // // return new NameField(fieldTopic, this, nameType); // } public OccurrenceType createOccurrenceType(String name) { TopicMap tm = this; TopicMapBuilderIF builder = tm.getTopicMapIF().getBuilder(); // create occurrence type TopicIF occurrenceTypeTopic = createNamedTopic(name, OntopolyModelUtils.getTopicIF(tm, PSI.ON, "occurrence-type")); OccurrenceType occurrenceType = new OccurrenceType(occurrenceTypeTopic, tm); // create occurrence field TopicIF occurrenceFieldType = OntopolyModelUtils.getTopicIF(tm, PSI.ON, "occurrence-field"); TopicIF occurrenceFieldTopic = builder.makeTopic(occurrenceFieldType); // on:has-occurrence-type($TT : on:occurrence-type, $FD : on:occurrence-field) final TopicIF HAS_OCCURRENCE_TYPE = OntopolyModelUtils.getTopicIF(tm, PSI.ON, "has-occurrence-type"); final TopicIF OCCURRENCE_TYPE = OntopolyModelUtils.getTopicIF(tm, PSI.ON, "occurrence-type"); final TopicIF OCCURRENCE_FIELD = OntopolyModelUtils.getTopicIF(tm, PSI.ON, "occurrence-field"); OntopolyModelUtils.makeBinaryAssociation(HAS_OCCURRENCE_TYPE, occurrenceTypeTopic, OCCURRENCE_TYPE, occurrenceFieldTopic, OCCURRENCE_FIELD); return occurrenceType; } // public OccurrenceField getOccurrenceField(OccurrenceType occurrenceType) { // String query = "select $FD from on:has-occurrence-type(%type% : on:occurrence-type, $FD : on:occurrence-field) limit 1?"; // Map<String,TopicIF> params = Collections.singletonMap("type", occurrenceType.getTopicIF()); // // QueryMapper<TopicIF> qm = newQueryMapperNoWrap(); // TopicIF fieldTopic = qm.queryForObject(query, params); // if (fieldTopic == null) // throw new OntopolyModelRuntimeException("Could not find occurrence field for " + occurrenceType); // // return new OccurrenceField(fieldTopic, this, occurrenceType); // } public AssociationType createAssociationType(String name) { TopicMap tm = this; TopicMapBuilderIF builder = tm.getTopicMapIF().getBuilder(); // create association type TopicIF associationTypeTopic = createNamedTopic(name, OntopolyModelUtils.getTopicIF(tm, PSI.ON, "association-type")); AssociationType associationType = new AssociationType(associationTypeTopic, tm); // create association field TopicIF associationFieldType = OntopolyModelUtils.getTopicIF(tm, PSI.ON, "association-field"); TopicIF associationFieldTopic = builder.makeTopic(associationFieldType); // on:has-association-type($TT : on:association-type, $FD : on:association-field) final TopicIF HAS_ASSOCIATION_TYPE = OntopolyModelUtils.getTopicIF(tm, PSI.ON, "has-association-type"); final TopicIF ASSOCIATION_TYPE = OntopolyModelUtils.getTopicIF(tm, PSI.ON, "association-type"); final TopicIF ASSOCIATION_FIELD = OntopolyModelUtils.getTopicIF(tm, PSI.ON, "association-field"); OntopolyModelUtils.makeBinaryAssociation(HAS_ASSOCIATION_TYPE, associationType.getTopicIF(), ASSOCIATION_TYPE, associationFieldTopic, ASSOCIATION_FIELD); final TopicIF HAS_ASSOCIATION_FIELD = OntopolyModelUtils.getTopicIF(tm, PSI.ON, "has-association-field"); final TopicIF ROLE_FIELD = OntopolyModelUtils.getTopicIF(tm, PSI.ON, "role-field"); TopicIF roleFieldType = OntopolyModelUtils.getTopicIF(tm, PSI.ON, "role-field"); // create first role field TopicIF roleFieldTopic1 = builder.makeTopic(roleFieldType); // on:has-association-field($AF : on:association-field, $FD : on:role-field) OntopolyModelUtils.makeBinaryAssociation(HAS_ASSOCIATION_FIELD, roleFieldTopic1, ROLE_FIELD, associationFieldTopic, ASSOCIATION_FIELD); // create second role field TopicIF roleFieldTopic2 = builder.makeTopic(roleFieldType); // on:has-association-field($AF : on:association-field, $FD : on:role-field) OntopolyModelUtils.makeBinaryAssociation(HAS_ASSOCIATION_FIELD, roleFieldTopic2, ROLE_FIELD, associationFieldTopic, ASSOCIATION_FIELD); return associationType; } // public AssociationField getAssociationField(AssociationType atype) { // // String query = "select $AF from " // + "on:has-association-type(%atype% : on:association-type, $AF : on:association-field) limit 1?"; // Map<String,TopicIF> params = Collections.singletonMap("atype", atype.getTopicIF()); // // QueryMapper<TopicIF> qm = newQueryMapperNoWrap(); // TopicIF fieldTopic = qm.queryForObject(query, params); // if (fieldTopic == null) // throw new OntopolyModelRuntimeException("Could not find association field for " + atype); // // return new AssociationField(fieldTopic, this, atype); // } public RoleType createRoleType(String name) { TopicIF type = OntopolyModelUtils.getTopicIF(this, PSI.ON, "role-type"); return new RoleType(createNamedTopic(name, type), this); } // public RoleField getRoleField(final AssociationType atype, final RoleType rtype) { // // String query = "select $AF, $RF from " // + "on:has-association-type(%atype% : on:association-type, $AF : on:association-field), " // + "on:has-association-field($AF : on:association-field, $RF : on:role-field), " // + "on:has-role-type($RF : on:role-field, %rtype% : on:role-type) limit 1?"; // Map<String,TopicIF> params = new HashMap<String,TopicIF>(2); // params.put("atype", atype.getTopicIF()); // params.put("rtype", rtype.getTopicIF()); // // QueryMapper<RoleField> qm = newQueryMapperNoWrap(); // RoleField roleField = qm.queryForObject(query, // new RowMapperIF<RoleField>() { // public RoleField mapRow(QueryResultIF result, int rowno) { // TopicIF associationFieldTopic = (TopicIF)result.getValue(0); // TopicIF roleFieldTopic = (TopicIF)result.getValue(1); // return new RoleField(roleFieldTopic, TopicMap.this, rtype, new AssociationField(associationFieldTopic, TopicMap.this, atype)); // } // }, params); // // if (roleField == null) // throw new OntopolyModelRuntimeException("Could not find field for " + atype + " and " + rtype); // // return roleField; // } /** * Returns the topics that matches the given search term. Only topics of * allowed player types are returned. * * @return a collection of Topic objects */ public List<Topic> searchAll(String searchTerm) { String query = "select $topic, $score from " + "topic-name($topic, $tn), value-like($tn, %searchTerm%, $score) " + "order by $score desc, $topic?"; Map<String,String> params = new HashMap<String,String>(); params.put("searchTerm", searchTerm); QueryMapper<Topic> qm = newQueryMapperNoWrap(); List<Topic> rows = qm.queryForList(query, params); Iterator<Topic> it = rows.iterator(); List<Topic> results = new ArrayList<Topic>(rows.size()); Collection<TopicIF> duplicateChecks = new HashSet<TopicIF>(rows.size()); while (it.hasNext()) { TopicIF topic = (TopicIF) it.next(); if (duplicateChecks.contains(topic)) continue; // avoid duplicates results.add(new Topic(topic, this)); duplicateChecks.add(topic); } return results; } /** * Package-internal method to get the name of a topic. Introduced in * Ontopia 5.1 for field definitions. Not used everywhere because I * don't understand the code well enough to do that, and anyway * things appear to work properly elsewhere. Should probably use this * in Topic.getName(), though. */ String getTopicName(TopicIF topic, AbstractTypingTopic fallback) { if (defnametype == null) defnametype = OntopolyModelUtils.getTopicIF(this, PSI.TMDM_TOPIC_NAME); int score = -10; TopicNameIF best = null; for (TopicNameIF name : topic.getTopicNames()) { int points = 0; if (name.getType() == defnametype) points += 10; points -= name.getScope().size(); if (points > score) { score = points; best = name; } } if (best != null) return best.getValue(); return (fallback == null ? null : fallback.getName()); } }