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 }