001 // This file is part of the Attempto Java Packages.
002 // Copyright 2008, Attempto Group, University of Zurich (see http://attempto.ifi.uzh.ch).
003 //
004 // The Attempto Java Packages is free software: you can redistribute it and/or modify it under the
005 // terms of the GNU Lesser General Public License as published by the Free Software Foundation,
006 // either version 3 of the License, or (at your option) any later version.
007 //
008 // The Attempto Java Packages is distributed in the hope that it will be useful, but WITHOUT ANY
009 // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
010 // PURPOSE. See the GNU Lesser General Public License for more details.
011 //
012 // You should have received a copy of the GNU Lesser General Public License along with the Attempto
013 // Java Packages. If not, see http://www.gnu.org/licenses/.
014
015 package ch.uzh.ifi.attempto.acewiki.core.ontology;
016
017 import java.io.File;
018 import java.io.FileInputStream;
019 import java.io.FileOutputStream;
020 import java.io.IOException;
021 import java.io.StringWriter;
022 import java.net.URI;
023 import java.net.URISyntaxException;
024 import java.util.ArrayList;
025 import java.util.Collection;
026 import java.util.HashMap;
027 import java.util.HashSet;
028 import java.util.Hashtable;
029 import java.util.List;
030 import java.util.Set;
031
032 import org.coode.owlapi.owlxml.renderer.OWLXMLRenderer;
033 import org.mindswap.pellet.owlapi.Reasoner;
034 import org.semanticweb.owl.apibinding.OWLManager;
035 import org.semanticweb.owl.io.StringInputSource;
036 import org.semanticweb.owl.model.OWLClass;
037 import org.semanticweb.owl.model.OWLIndividual;
038 import org.semanticweb.owl.model.OWLOntology;
039 import org.semanticweb.owl.model.OWLOntologyCreationException;
040 import org.semanticweb.owl.model.OWLOntologySetProvider;
041 import org.semanticweb.owl.model.OWLSubClassAxiom;
042 import org.semanticweb.owl.util.OWLOntologyMerger;
043
044 import uk.ac.manchester.cs.owl.OWLClassImpl;
045 import uk.ac.manchester.cs.owl.OWLDataFactoryImpl;
046 import ch.uzh.ifi.attempto.acewiki.Logger;
047
048 /**
049 * This class represents an AceWiki ontology which consists of ontology element definitions and
050 * of ontological statements. Each ontology element has its own article that consists of ontological
051 * statements.
052 *
053 * @author Tobias Kuhn
054 */
055 public class Ontology {
056
057 private static final HashMap<String, Ontology> ontologies = new HashMap<String, Ontology>();
058
059 private ArrayList<OntologyElement> elements = new ArrayList<OntologyElement>();
060 private Hashtable<String, OntologyElement> wordIndex = new Hashtable<String, OntologyElement>();
061 private Hashtable<Long, OntologyElement> idIndex = new Hashtable<Long, OntologyElement>();
062
063 private final String name;
064 private final String baseURI;
065 private long idCount = 0;
066 private long stateID = 0;
067
068 private Reasoner reasoner;
069 private OWLOntology differentIndividualsAxiom;
070
071 /**
072 * Creates a new empty ontology with the given name and base URI.
073 *
074 * @param name The name of the ontology.
075 * @param baseURI The base URI that is used to identify the ontology elements.
076 */
077 private Ontology(String name, String baseURI) {
078 this.name = name.toString(); // null value throws an exception
079 this.baseURI = baseURI;
080 if (baseURI == null) {
081 baseURI = "";
082 }
083 ontologies.put(name, this);
084 }
085
086 /**
087 * Loads an ontology (or creates an empty ontology if the ontology cannot be found). The complete
088 * URI of the ontology is baseURI + name.
089 *
090 * @param name The name of the ontology.
091 * @param baseURI The base URI that is used to identify the ontology elements.
092 * @return The loaded ontology.
093 */
094 public synchronized static Ontology loadOntology(String name, String baseURI) {
095 if (ontologies.get(name) != null) {
096 return ontologies.get(name);
097 }
098 Ontology ontology = new Ontology(name, baseURI);
099 ontology.log("loading ontology");
100 File dataDir = new File("data/" + name);
101 if (dataDir.exists()) {
102 for (File file : dataDir.listFiles()) {
103 try {
104 long id = new Long(file.getName());
105 ontology.log("reading file: " + file.getName());
106 FileInputStream in = new FileInputStream(file);
107 byte[] bytes = new byte[in.available()];
108 in.read(bytes);
109 in.close ();
110 String s = new String(bytes);
111 OntologyElement.loadOntologyElement(s, id, ontology);
112 } catch (NumberFormatException ex) {
113 ontology.log("ignoring file: " + file.getName());
114 } catch (IOException ex) {
115 ontology.log("cannot read file: " + file.getName());
116 }
117 }
118 } else {
119 ontology.log("no data found; blank ontology is created");
120 }
121 ontology.refreshReasoner();
122 return ontology;
123 }
124
125 synchronized void save(OntologyElement oe) {
126 if (!(new File("data")).exists()) (new File("data")).mkdir();
127 if (!(new File("data/" + name)).exists()) (new File("data/" + name)).mkdir();
128
129 if (!elements.contains(oe)) {
130 (new File("data/" + name + "/" + oe.getId())).delete();
131 return;
132 }
133
134 try {
135 FileOutputStream out = new FileOutputStream("data/" + name + "/" + oe.getId());
136 out.write(oe.serialize().getBytes("UTF-8"));
137 out.close();
138 } catch (IOException ex) {
139 ex.printStackTrace();
140 }
141 }
142
143 synchronized void register(OntologyElement element) {
144 if (elements.contains(element)) {
145 log("error: element already registered");
146 throw new RuntimeException("Registration failed: Element is already registered.");
147 }
148
149 log("register: " + element);
150 stateID++;
151
152 if (element.getId() < 0) {
153 element.setId(nextId());
154 }
155 elements.add(element);
156 idIndex.put(element.getId(), element);
157 if (element.getId() > idCount) idCount = element.getId();
158
159 for (String word : element.getWords()) {
160 if (word == null) continue;
161
162 if (wordIndex.get(word) == null) {
163 wordIndex.put(word, element);
164 } else if (wordIndex.get(word) != element) {
165 log("error: word already used");
166 throw new RuntimeException("Registration failed: The word '" + word + "' is already used.");
167 }
168 }
169
170 if (element instanceof Individual) {
171 updateDifferentIndividualsAxiom();
172 }
173
174 }
175
176 synchronized void removeFromWordIndex(OntologyElement oe) {
177 for (String word : oe.getWords()) {
178 if (word != null) {
179 wordIndex.remove(word);
180 }
181 }
182 }
183
184 synchronized void addToWordIndex(OntologyElement oe) {
185 for (String word : oe.getWords()) {
186 if (word != null) {
187 if (wordIndex.get(word) == null) {
188 wordIndex.put(word, oe);
189 } else if (wordIndex.get(word) != oe) {
190 throw new RuntimeException("Word update failed: The word '" + word + "' is already used.");
191 }
192 }
193 }
194 }
195
196 /**
197 * Removes the given ontology element from the ontology.
198 *
199 * @param element The ontology element to be removed.
200 */
201 public synchronized void remove(OntologyElement element) {
202 if (!elements.contains(element)) {
203 log("error: unknown element");
204 return;
205 }
206
207 log("remove: " + element.getWord());
208 stateID++;
209
210 for (String word : element.getWords()) {
211 if (word == null) continue;
212 wordIndex.remove(word);
213 }
214 elements.remove(element);
215 idIndex.remove(element.getId());
216 for (Sentence s : element.getSentences()) {
217 retractSentence(s);
218 }
219 save(element);
220
221 if (element instanceof Individual) {
222 updateDifferentIndividualsAxiom();
223 }
224
225 }
226
227 /**
228 * Returns all the sentences that use the given word form (by word number) of the given
229 * ontology element.
230 *
231 * @param element The ontology element.
232 * @param wordNumber The word number.
233 * @return A list of all sentence that contain the word.
234 */
235 public synchronized List<Sentence> getReferences(OntologyElement element, int wordNumber) {
236 List<Sentence> list = new ArrayList<Sentence>();
237 for (OntologyElement el : elements) {
238 for (Sentence s : el.getSentences()) {
239 if ((wordNumber == -1 && s.contains(element)) || (wordNumber > -1 && s.contains(element, wordNumber))) {
240 list.add(s);
241 }
242 }
243 }
244 return list;
245 }
246
247 /**
248 * Returns all the sentences that use the given ontology element (no matter which word form
249 * is used).
250 *
251 * @param element The ontology element.
252 * @return A list of all sentence that contain the ontology element.
253 */
254 public synchronized List<Sentence> getReferences(OntologyElement element) {
255 return getReferences(element, -1);
256 }
257
258 /**
259 * Returns the ontology element with the given name, or null if there is no such element.
260 *
261 * @param name The name of the ontology element.
262 * @return The ontology element.
263 */
264 public OntologyElement get(String name) {
265 return wordIndex.get(name);
266 }
267
268 OntologyElement get(long id) {
269 return idIndex.get(id);
270 }
271
272 /**
273 * Returns all ontology elements.
274 *
275 * @return A collection of all ontology elements.
276 */
277 public Collection<OntologyElement> getOntologyElements() {
278 return new ArrayList<OntologyElement>(elements);
279 }
280
281 /**
282 * Returns true if the given ontology element is contained by the ontology (identity check).
283 *
284 * @param ontologyElement The ontology element.
285 * @return true if the ontology element is contained by the ontology.
286 */
287 public boolean contains(OntologyElement ontologyElement) {
288 return elements.contains(ontologyElement);
289 }
290
291 /**
292 * Returns the name of the ontology.
293 *
294 * @return The name of the ontology.
295 */
296 public String getName() {
297 return name;
298 }
299
300 /**
301 * Returns the URI of the ontology (baseURI + name).
302 *
303 * @return The URI of the ontology.
304 */
305 public String getURI() {
306 return baseURI + name;
307 }
308
309 /**
310 * Returns the complete ontology as an OWL/XML formatted string. All sentences that participate in
311 * reasoning are included.
312 *
313 * @return A string that contains the complete ontology in OWL/XML format.
314 */
315 public synchronized String getOWLOntologyAsXML() {
316 StringWriter sw = new StringWriter();
317 try {
318 OWLXMLRenderer renderer = new OWLXMLRenderer(OWLManager.createOWLOntologyManager());
319 renderer.render(getOWLOntology(), sw);
320 sw.close();
321 } catch (Exception ex) {
322 ex.printStackTrace();
323 }
324 return sw.toString();
325 }
326
327 /**
328 * Returns an OWL ontology object that contains the complete ontology. More precisely, all sentences
329 * that participate in reasoning are included.
330 *
331 * @return An OWL ontology object containing the complete ontology.
332 */
333 public synchronized OWLOntology getOWLOntology() {
334 OWLOntologySetProvider setProvider = new OWLOntologySetProvider() {
335
336 public Set<OWLOntology> getOntologies() {
337 HashSet<OWLOntology> ontologies = new HashSet<OWLOntology>();
338 for (OntologyElement el : elements) {
339 for (Sentence s : el.getSentences()) {
340 if (!s.isReasonerParticipant()) continue;
341
342 try {
343 ontologies.add(s.getOWLOntology());
344 } catch (OWLOntologyCreationException ex) {
345 ex.printStackTrace();
346 }
347 }
348 }
349 ontologies.add(differentIndividualsAxiom);
350 return ontologies;
351 }
352
353 };
354
355 URI uri = null;
356 try {
357 uri = new URI("http://attempto.ifi.uzh.ch/default/");
358 uri = new URI(getURI());
359 } catch (URISyntaxException ex) {
360 ex.printStackTrace();
361 }
362
363 OWLOntology owlOntology = null;
364 try {
365 OWLOntologyMerger ontologyMerger = new OWLOntologyMerger(setProvider);
366 owlOntology = ontologyMerger.createMergedOntology(OWLManager.createOWLOntologyManager(), uri);
367 } catch (Exception ex) {
368 ex.printStackTrace();
369 }
370 return owlOntology;
371 }
372
373 private synchronized void refreshReasoner() {
374 log("refresh reasoner");
375
376 if (reasoner == null) {
377 reasoner = new Reasoner(OWLManager.createOWLOntologyManager());
378 } else {
379 reasoner.clearOntologies();
380 }
381 updateDifferentIndividualsAxiom();
382
383 log("reasoner: loading statements");
384 HashSet<OWLOntology> ontologies = new HashSet<OWLOntology>();
385
386 for (OntologyElement oe : elements) {
387 for (Sentence s : oe.getSentences()) {
388 if (s.isReasonerParticipant() && s.isIntegrated()) {
389 try {
390 ontologies.add(s.getOWLOntology());
391 } catch (Exception ex) {
392 ex.printStackTrace();
393 }
394 }
395 }
396 }
397 reasoner.loadOntologies(ontologies);
398 log("reasoner: statements loaded");
399 }
400
401 /**
402 * Refreshes the given ontology element. All sentences that use the ontology element are
403 * updated.
404 *
405 * @param element The ontology element to be refreshed.
406 */
407 synchronized void refresh(OntologyElement element) {
408 for (Sentence s : getReferences(element)) {
409 if (s.isIntegrated()) {
410 retractSentence(s);
411 s.parse();
412 commitSentence(s);
413 } else {
414 s.parse();
415 }
416 }
417 save(element);
418 }
419
420 /**
421 * Uses the ontology manager to read an OWL ontology from a string (that contains an ontology
422 * in OWL-XML format).
423 *
424 * @param owlxml The serialized OWL-XML ontology.
425 * @return The OWL ontology object.
426 * @throws OWLOntologyCreationException If the string cannot be parsed.
427 */
428 public OWLOntology readOWLOntology(String owlxml) throws OWLOntologyCreationException {
429 return OWLManager.createOWLOntologyManager().loadOntology(new StringInputSource(owlxml));
430 }
431
432 /**
433 * Commits the sentence. This means that it is added to the reasoner. An integer value is returned
434 * that denotes the success or failure of the operation:
435 * 0 is returned if the operation succeeds.
436 * 1 is returned if it fails because the sentence introduces inconsistency into the knowledge base.
437 * 2 is returned if the reasoner runs out of memory (this can occur sometimes with large ontologies).
438 *
439 * @param sentence The sentence to be commited.
440 * @return An integer value denoting the success/failure of the operation.
441 */
442 synchronized int commitSentence(Sentence sentence) {
443 if (reasoner == null || sentence == null || sentence.isIntegrated()) return 0;
444
445 if (!sentence.isReasonerParticipant()) {
446 sentence.setIntegrated(true);
447 return 0;
448 }
449
450 log("commit sentence");
451
452 boolean isConsistent;
453 try {
454 reasoner.loadOntology(sentence.getOWLOntology());
455 log("check for consistency");
456 isConsistent = reasoner.isConsistent();
457 log("is consistent: " + isConsistent);
458 } catch (OWLOntologyCreationException ex) {
459 log("unexpected error");
460 ex.printStackTrace();
461 refreshReasoner();
462 stateID++;
463 return 0;
464 } catch (OutOfMemoryError err) {
465 log("error: out of memory");
466 System.gc();
467 refreshReasoner();
468 return 2;
469 }
470
471 if (isConsistent) {
472 sentence.setIntegrated(true);
473 stateID++;
474 return 0;
475 } else {
476 try {
477 reasoner.unloadOntology(sentence.getOWLOntology());
478 } catch (OWLOntologyCreationException ex) {
479 log("unexpected error");
480 ex.printStackTrace();
481 refreshReasoner();
482 }
483 return 1;
484 }
485 }
486
487 /**
488 * Retracts the sentence. This means that the sentence is removed from the reasoner.
489 *
490 * @param sentence The sentence to be retracted.
491 */
492 synchronized void retractSentence(Sentence sentence) {
493 if (
494 reasoner == null ||
495 sentence == null ||
496 !sentence.isIntegrated() ||
497 !sentence.isReasonerParticipant()
498 ) return;
499
500 log("retract sentence");
501 stateID++;
502
503 try {
504 reasoner.unloadOntology(sentence.getOWLOntology());
505 sentence.setIntegrated(false);
506 } catch (OWLOntologyCreationException ex) {
507 log("unexpected error");
508 ex.printStackTrace();
509 refreshReasoner();
510 }
511
512 }
513
514 void log(String text) {
515 Logger.log(name, "onto", 0, "onto", text);
516 }
517
518 /**
519 * Updates the axiom that states that all named individuals are different. Thus, unique
520 * name assumption is applied.
521 */
522 private synchronized void updateDifferentIndividualsAxiom() {
523 if (reasoner == null) return;
524
525 reasoner.unloadOntology(differentIndividualsAxiom);
526
527 String owlString =
528 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n" +
529 "<Ontology " +
530 "xml:base=\"http://www.w3.org/2006/12/owl11-xml#\" " +
531 "xmlns=\"http://www.w3.org/2006/12/owl11-xml#\" " +
532 "URI=\"" + getURI() + "/different_individuals/" + stateID + "\">\n" +
533 "\t<DifferentIndividuals>\n";
534 for (OntologyElement oe : getOntologyElements()) {
535 if (oe instanceof Individual) {
536 String word = ((Individual) oe).getWord();
537 if (word.startsWith("the ")) word = word.substring(4);
538 owlString += "\t\t<Individual URI=\"" + ((Individual) oe).getURI() + "\" />\n";
539 }
540 }
541 owlString +=
542 "\t</DifferentIndividuals>\n" +
543 "</Ontology>";
544
545 try {
546 differentIndividualsAxiom = readOWLOntology(owlString);
547 reasoner.loadOntology(differentIndividualsAxiom);
548 } catch (OWLOntologyCreationException ex) {
549 log("unexpected error");
550 ex.printStackTrace();
551 }
552 }
553
554 /**
555 * Returns all concepts the given individual belongs to. The reasoner is used for this.
556 *
557 * @param ind The individual.
558 * @return A list of all concepts of the individual.
559 * @see Individual#getConcepts()
560 */
561 public synchronized List<Concept> getConcepts(Individual ind) {
562 OWLIndividual owlIndividual = (new OWLDataFactoryImpl()).getOWLIndividual(ind.getURI());
563 Set<Set<OWLClass>> owlClasses = reasoner.getTypes(owlIndividual, false);
564 ArrayList<Concept> concepts = new ArrayList<Concept>();
565 for (Set<OWLClass> s : owlClasses) {
566 for (OWLClass oc : s) {
567 if (oc.isOWLThing() || oc.isOWLNothing()) continue;
568 String conceptURI = oc.getURI().toASCIIString();
569 String conceptName = conceptURI.substring(conceptURI.indexOf("#") + 1);
570 concepts.add((Concept) get(conceptName));
571 }
572 }
573 return concepts;
574 }
575
576 /**
577 * Returns all individuals that belong to the given concept. The reasoner is used for this.
578 *
579 * @param concept The concept.
580 * @return A list of all individuals of the concept.
581 * @see Concept#getIndividuals()
582 */
583 public synchronized List<Individual> getIndividuals(Concept concept) {
584 OWLClass owlClass = new OWLClassImpl(new OWLDataFactoryImpl(), concept.getURI());
585 Set<OWLIndividual> owlIndividuals = reasoner.getIndividuals(owlClass, false);
586 ArrayList<Individual> individuals = new ArrayList<Individual>();
587 for (OWLIndividual oi : owlIndividuals) {
588 String indURI = oi.getURI().toASCIIString();
589 String indName = indURI.substring(indURI.indexOf("#") + 1);
590 if (!indName.matches("I[0-9]+")) {
591 individuals.add((Individual) get(indName));
592 }
593 }
594 return individuals;
595 }
596
597 /**
598 * Returns all super-concepts of the given concept. The reasoner is used for this.
599 *
600 * @param concept The concept for which all super-concepts should be returned.
601 * @return A list of all super-concepts.
602 * @see Concept#getSuperConcepts()
603 */
604 public synchronized List<Concept> getSuperConcepts(Concept concept) {
605 OWLClass owlClass = new OWLClassImpl(new OWLDataFactoryImpl(), concept.getURI());
606 Set<Set<OWLClass>> owlClasses = reasoner.getAncestorClasses(owlClass);
607 ArrayList<Concept> concepts = new ArrayList<Concept>();
608 for (Set<OWLClass> s : owlClasses) {
609 for (OWLClass oc : s) {
610 if (oc.isOWLThing() || oc.isOWLNothing()) continue;
611 String conceptURI = oc.getURI().toASCIIString();
612 String conceptName = conceptURI.substring(conceptURI.indexOf("#") + 1);
613 concepts.add((Concept) get(conceptName));
614 }
615 }
616 return concepts;
617 }
618
619 /**
620 * Returns all the sub-concepts of the given concept. The reasoner is used for this.
621 *
622 * @param concept The concept for which all sub-concepts should be returned.
623 * @return A list of all sub-concepts.
624 * @see Concept#getSubConcepts()
625 */
626 public synchronized List<Concept> getSubConcepts(Concept concept) {
627 OWLClass owlClass = new OWLClassImpl(new OWLDataFactoryImpl(), concept.getURI());
628 Set<Set<OWLClass>> owlClasses = reasoner.getDescendantClasses(owlClass);
629 ArrayList<Concept> concepts = new ArrayList<Concept>();
630 for (Set<OWLClass> s : owlClasses) {
631 for (OWLClass oc : s) {
632 if (oc.isOWLThing() || oc.isOWLNothing()) continue;
633 String conceptURI = oc.getURI().toASCIIString();
634 String conceptName = conceptURI.substring(conceptURI.indexOf("#") + 1);
635 concepts.add((Concept) get(conceptName));
636 }
637 }
638 return concepts;
639 }
640
641 /**
642 * Returns all individuals that answer the given question. The reasoner is used for this.
643 * Questions in AceWiki are "DL Queries". They describe a concept and the answer consists
644 * of all individuals that belong to this concept. The null value is returned if the
645 * sentence is not a question.
646 *
647 * @param questionSentence The question sentence that should be answered.
648 * @return A list of individuals that are the answer for the question.
649 * @see Sentence#getAnswer()
650 */
651 public synchronized List<Individual> getAnswer(Sentence questionSentence) {
652 if (!questionSentence.isQuestion()) return null;
653
654 ArrayList<Individual> individuals = new ArrayList<Individual>();
655 try {
656 OWLSubClassAxiom answerOWLAxiom = (OWLSubClassAxiom) questionSentence.getOWLOntology().getAxioms().iterator().next();
657
658 Set<OWLIndividual> owlIndividuals = reasoner.getIndividuals(answerOWLAxiom.getSubClass(), false);
659 for (OWLIndividual oi : owlIndividuals) {
660 String indURI = oi.getURI().toASCIIString();
661 String indName = indURI.substring(indURI.indexOf("#") + 1);
662 if (!indName.matches("I[0-9]+")) {
663 individuals.add((Individual) get(indName));
664 }
665 }
666 } catch (Exception ex) {
667 ex.printStackTrace();
668 }
669 return individuals;
670 }
671
672 /**
673 * Returns true if the ontology is consistent. If nothing goes wrong, this should always return true.
674 * The reasoner is used for this.
675 *
676 * @return true if the ontology is consistent.
677 */
678 public synchronized boolean isConsistent() {
679 return reasoner.isConsistent();
680 }
681
682 /**
683 * Checks if the given concept is satisfiable. The reasoner is used for this.
684 *
685 * @param concept The concept.
686 * @return true if the concept is satisfiable.
687 */
688 public synchronized boolean isSatisfiable(Concept concept) {
689 OWLClass owlClass = new OWLClassImpl(new OWLDataFactoryImpl(), concept.getURI());
690 return (!reasoner.isDefined(owlClass) || reasoner.isSatisfiable(owlClass));
691 }
692
693 private long nextId() {
694 return ++idCount;
695 }
696
697 /**
698 * Returns the state id of the ontology. This id increases each time the ontology changes (more precisely:
699 * each time the part of the ontology that participates in reasoning changes). This id is used to find out
700 * whether cached information is still valid or has to be recalculated.
701 *
702 * @return The state id of the ontology.
703 */
704 long getStateID() {
705 return stateID;
706 }
707
708 }