content: // This file is part of AceWiki.
  // Copyright 2008-2013, AceWiki developers.
  // 
  // AceWiki is free software: you can redistribute it and/or modify it under the terms of the GNU
  // Lesser General Public License as published by the Free Software Foundation, either version 3 of
  // the License, or (at your option) any later version.
  // 
  // AceWiki is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
  // even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  // Lesser General Public License for more details.
  // 
  // You should have received a copy of the GNU Lesser General Public License along with AceWiki. If
  // not, see http://www.gnu.org/licenses/.
  
  package ch.uzh.ifi.attempto.acewiki.core;
  
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.TreeMap;
  
  import com.google.common.base.Splitter;
  import com.google.common.collect.ImmutableSet;
  import com.google.common.collect.Iterables;
  
  import ch.uzh.ifi.attempto.base.LoggerContext;
  
  /**
   * This class represents an AceWiki ontology which consists of ontology element definitions and
   * of ontological statements. Each ontology element has its own article that consists of
   * ontological statements.
   * 
   * @author Tobias Kuhn
   */
  public class Ontology {
  
  	private AceWikiEngine engine;
  	private CachingReasoner reasoner;
  	private StatementFactory statementFactory;
  	private AceWikiStorage storage;
  	private LoggerContext loggerContext;
  	private final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(this.getClass());
  
  	private Map<Long, OntologyElement> idIndex = new TreeMap<Long, OntologyElement>();
  
  	private final String name;
  	private final String baseURI;
  	private long idCount = 0;
  	private long stateID = 0;
  
  	private Map<String, String> parameters;
  
  	/**
  	 * Creates a new empty ontology with the given name and parameters.
  	 * 
  	 * @param name The name of the ontology.
  	 * @param parameters The parameters.
  	 */
  	Ontology(String name, Map<String, String> parameters, AceWikiStorage storage) {
  		this.name = name.toString();  // null value throws an exception
  		this.parameters = parameters;
  		this.storage = storage;
  
  		loggerContext = new LoggerContext(name, "onto", "0");
  
  		engine = AbstractAceWikiEngine.createLanguageEngine(this);
  		if (engine.getReasoner() == null) {
  			reasoner = new CachingReasoner(new DummyReasoner());
  		} else {
  			reasoner = new CachingReasoner(engine.getReasoner());
  		}
  		reasoner.init(this);
  		statementFactory = new StatementFactory(this);
  
  		String b = getParameter("baseuri");
  		if (b == null || b.equals("")) {
  			baseURI = "http://attempto.ifi.uzh.ch/acewiki/default/";
  		} else {
  			if (b.endsWith("/")) {
  				baseURI = b;
  			} else {
  				baseURI = b + "/";
  			}
  		}
  	}
  
  	/**
  	 * Returns the AceWiki engine.
  	 * 
  	 * @return The AceWiki engine.
  	 */
  	public AceWikiEngine getEngine() {
  		return engine;
  	}
  
  	/**
  	 * Returns the reasoner object in the form of a caching reasoner.
  	 * 
  	 * @return The reasoner.
  	 */
  	public CachingReasoner getReasoner() {
  		return reasoner;
  	}
  
  	/**
  	 * Returns the statement factory.
  	 * 
  	 * @return The statement factory.
  	 */
  	public StatementFactory getStatementFactory() {
  		return statementFactory;
  	}
  
  	/**
  	 * Returns the storage object.
  	 * 
  	 * @return The storage object.
  	 */
  	public AceWikiStorage getStorage() {
  		return storage;
  	}
  
  	/**
  	 * Registers the given ontology element.
  	 * 
  	 * @param element The ontology element to register.
  	 */
  	public synchronized void register(OntologyElement element) {
  		if (contains(element)) {
  			log("error: element already registered");
  			throw new RuntimeException("Registration failed: Element is already registered.");
  		}
  		element.initOntology(this);
  
  		log("register: " + element);
  		stateID++;
  
  		if (element.getId() == -1) {
  			element.initId(nextId());
  		}
  		idIndex.put(element.getId(), element);
  		if (element.getId() > idCount) idCount = element.getId();
  
  		engine.getWordIndex().elementAdded(element);
  
  		getReasoner().loadElement(element);
  		getReasoner().flushElements();
  
  		getStorage().save(element);
  	}
  
  	/**
  	 * Removes the given ontology element from the ontology.
  	 * 
  	 * @param element The ontology element to be removed.
  	 */
  	public synchronized void remove(OntologyElement element) {
  		if (!contains(element)) {
  			log("error: unknown element");
  			return;
  		}
  
  		log("remove: " + element.getWord());
  		stateID++;
  
  		engine.getWordIndex().elementRemoved(element);
  
  		idIndex.remove(element.getId());
  		for (Sentence s : element.getArticle().getSentences()) {
  			retractSentence(s);
  		}
  		storage.save(element);
  
  		getReasoner().unloadElement(element);
  		getReasoner().flushElements();
  	}
  
  	/**
  	 * Changes the word forms of the given ontology element.
  	 * 
  	 * @param element The ontology element to be changed.
  	 * @param serializedWords The serialized word forms.
  	 */
  	public synchronized void change(OntologyElement element, String serializedWords) {
  		if (contains(element)) {
  			engine.getWordIndex().elementBeforeChange(element);
  			getReasoner().unloadElement(element);
  			element.setWords(serializedWords);
  			engine.getWordIndex().elementAfterChange(element);
  			getReasoner().loadElement(element);
  			refresh(element);
  		} else {
  			element.setWords(serializedWords);
  		}
  	}
  
  	/**
  	 * Returns all the sentences that use the given ontology element.
  	 * 
  	 * @param element The ontology element.
  	 * @return A list of all sentence that contain the ontology element.
  	 */
  	public synchronized List<Sentence> getReferences(OntologyElement element) {
  		List<Sentence> list = new ArrayList<Sentence>();
  		for (OntologyElement el : idIndex.values()) {
  			for (Sentence s : el.getArticle().getSentences()) {
  				if (s.contains(element)) {
  					list.add(s);
  				}
  			}
  		}
  		return list;
  	}
  
  	/**
  	 * Returns the ontology element with the given name, or null if there is no such element.
  	 * 
  	 * @param name The name of the ontology element.
  	 * @return The ontology element.
  	 */
  	public synchronized OntologyElement getElement(String name) {
  		return engine.getWordIndex().getElement(name);
  	}
  
  	/**
  	 * Returns the ontology element with the given id, or null if there is no such element.
  	 * 
  	 * @param id The id of the ontology element.
  	 * @return The ontology element.
  	 */
  	public synchronized OntologyElement get(long id) {
  		return idIndex.get(id);
  	}
  
  	/**
  	 * Returns all ontology elements. The list is a copy of the internal list.
  	 * 
  	 * @return A list of all ontology elements.
  	 */
  	public synchronized List<OntologyElement> getOntologyElements() {
  		return new ArrayList<OntologyElement>(idIndex.values());
  	}
  
  
  	/**
  	 * Returns all instances of class <code>type</code> in the current list of ontology elements.
  	 * The returned iterable has elements whose class is <code>type</code> or a subclass of <code>type</code>.
  	 * The returned iterable's iterator does not support <code>remove()</code>.
  	 *
  	 * @param type the type of ontology elements desired
  	 * @return an unmodifiable iterable containing all the ontology elements that are of the requested type
  	 */
  	public synchronized <T> Iterable<T> getOntologyElements(Class<T> type) {
  		return Iterables.filter(idIndex.values(), type);
  	}
  
  
  	/**
  	 * Returns true if the given ontology element is contained by the ontology (identity check).
  	 * 
  	 * @param ontologyElement The ontology element.
  	 * @return true if the ontology element is contained by the ontology.
  	 */
  	public synchronized boolean contains(OntologyElement ontologyElement) {
  		return idIndex.containsValue(ontologyElement);
  	}
  
  	/**
  	 * Returns the name of the ontology.
  	 * 
  	 * @return The name of the ontology.
  	 */
  	public String getName() {
  		return name;
  	}
  
  	/**
  	 * Returns the URI of the ontology (baseURI + name).
  	 * 
  	 * @return The URI of the ontology.
  	 */
  	public String getURI() {
  		return baseURI + name;
  	}
  
  	/**
  	 * Refreshes the given ontology element. All sentences that use the ontology element are
  	 * updated.
  	 * 
  	 * @param element The ontology element to be refreshed.
  	 */
  	synchronized void refresh(OntologyElement element) {
  		for (Sentence s : getReferences(element)) {
  			if (s.isIntegrated()) {
  				retractSentence(s);
  				s.update();
  				commitSentence(s);
  			} else {
  				s.update();
  			}
  		}
  		storage.save(element);
  	}
  
  	/**
  	 * Commits the sentence. This means that it is added to the reasoner.
  	 * 
  	 * @param sentence The sentence to be commited.
  	 * @throws InconsistencyException if the sentence is inconsistent with the existing sentences.
  	 */
  	protected synchronized void commitSentence(Sentence sentence) throws InconsistencyException {
  		if (sentence == null || sentence.isIntegrated()) return;
  
  		if (!sentence.isReasonable()) {
  			sentence.setIntegrated(true);
  			return;
  		}
  
  		log("commit sentence");
  
  		try {
  			getReasoner().loadSentence(sentence);
  		} catch (InconsistencyException ex) {
  			log("not consistent!");
  			getReasoner().unloadSentence(sentence);
  			throw ex;
  		} catch (Throwable t) {
  			log("error encountered!");
  			t.printStackTrace();
  			System.gc();
  			getReasoner().unloadSentence(sentence);
  			return;
  		}
  
  		if (!getReasoner().isConsistent()) {
  			log("not consistent!");
  			getReasoner().unloadSentence(sentence);
  			throw new InconsistencyException();
  		}
  
  		log("consistent!");
  		sentence.setIntegrated(true);
  		stateID++;
  	}
  
  	/**
  	 * This method tries to reassert a sentence that is not yet integrated.
  	 * 
  	 * @param sentence The sentence to be reasserted.
  	 * @throws InconsistencyException if the sentence is inconsistent with the existing sentences.
  	 */
  	public synchronized void reassert(Sentence sentence) throws InconsistencyException {
  		commitSentence(sentence);
  		getStorage().save(sentence.getArticle().getOntologyElement());
  	}
  
  	/**
  	 * Retracts the sentence. This means that the sentence is removed from the reasoner.
  	 * 
  	 * @param sentence The sentence to be retracted.
  	 */
  	protected synchronized void retractSentence(Sentence sentence) {
  		if (
  				sentence == null ||
  				!sentence.isIntegrated() ||
  				!sentence.isReasonable()
  				) return;
  
  		log("retract sentence");
  		stateID++;
  		getReasoner().unloadSentence(sentence);
  		sentence.setIntegrated(false);
  	}
  
  	/**
  	 * This method retracts an integrated sentence so that it is still part of the wiki
  	 * article but does not participate in reasoning anymore.
  	 * 
  	 * @param sentence The sentence to be retracted.
  	 */
  	public synchronized void retract(Sentence sentence) {
  		retractSentence(sentence);
  		getStorage().save(sentence.getArticle().getOntologyElement());
  	}
  
  	/**
  	 * Writes a log entry.
  	 * 
  	 * @param text Log text.
  	 */
  	public void log(String text) {
  		loggerContext.propagateWithinThread();
  		org.slf4j.MDC.put("type", "onto");
  		log.info(text);
  	}
  
  	private long nextId() {
  		return ++idCount;
  	}
  
  	/**
  	 * Returns the state id of the ontology. This id increases each time the ontology changes (more
  	 * precisely: each time the part of the ontology that participates in reasoning changes). This
  	 * id is used to find out whether cached information is still valid or has to be recalculated.
  	 * 
  	 * @return The state id of the ontology.
  	 */
  	public long getStateID() {
  		return stateID;
  	}
  
  	/**
  	 * Returns a parameter value.
  	 * 
  	 * @param name The parameter name.
  	 * @return The parameter value.
  	 */
  	public String getParameter(String name) {
  		return parameters.get(name);
  	}
  
  
  	/**
  	 * Returns that parameter value as an integer, if it is
  	 * parseable as an integer, otherwise returns 0.
  	 */
  	public int getParameterAsInt(String name) {
  		String valueAsStr = getParameter(name);
  		if (valueAsStr != null) {
  			try {
  				return Integer.parseInt(valueAsStr);
  			} catch (NumberFormatException e) {};
  		}
  		return 0;
  	}
  
  
  	/**
  	 * Returns the web.xml parameter interpreted as a set of {@code String}s.
  	 * Elements must be comma-separated and whitespace is ignored.
  	 *
  	 * @param paramName The parameter name.
  	 * @return The value of the parameter.
  	 */
  	public Set<String> getParameterAsSetOfString(String paramName) {
  		String valueAsStr = getParameter(paramName);
  		if (valueAsStr == null) {
  			return ImmutableSet.of();
  		}
  		return ImmutableSet.copyOf(Splitter.on(',').omitEmptyStrings().trimResults().split(valueAsStr));
  	}
  
  
  	/**
  	 * Returns the language that should be used to express the wiki sentences in the log.
  	 * In multilingual wikis this language should express the edited sentences
  	 * as clearly and readably as possible, i.e. it should be based on English
  	 * and avoid ambiguity. In ACE-based wikis, ACE is a good candidate.
  	 */
  	public String getLoggingLanguage() {
  		String loggingLanguage = getParameter("logging_language");
  		if (loggingLanguage == null) {
  			return getEngine().getLanguages()[0];
  		}
  		return loggingLanguage;
  	}
  
  }