View Javadoc
1   /*
2    * The basecode project
3    *
4    * Copyright (c) 2007-2019 University of British Columbia
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *       http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   *
18   */
19  
20  package ubic.basecode.ontology.jena;
21  
22  import com.hp.hpl.jena.ontology.*;
23  import com.hp.hpl.jena.rdf.model.ModelFactory;
24  import com.hp.hpl.jena.rdf.model.NodeIterator;
25  import com.hp.hpl.jena.rdf.model.Property;
26  import com.hp.hpl.jena.rdf.model.Resource;
27  import com.hp.hpl.jena.rdfxml.xmlinput.ARPErrorNumbers;
28  import com.hp.hpl.jena.rdfxml.xmlinput.ParseException;
29  import com.hp.hpl.jena.reasoner.ReasonerFactory;
30  import com.hp.hpl.jena.reasoner.rulesys.OWLFBRuleReasonerFactory;
31  import com.hp.hpl.jena.reasoner.rulesys.OWLMicroReasonerFactory;
32  import com.hp.hpl.jena.reasoner.rulesys.OWLMiniReasonerFactory;
33  import com.hp.hpl.jena.reasoner.transitiveReasoner.TransitiveReasonerFactory;
34  import com.hp.hpl.jena.util.iterator.ExtendedIterator;
35  import com.hp.hpl.jena.vocabulary.DC_11;
36  import org.apache.commons.lang3.RandomStringUtils;
37  import org.apache.commons.lang3.StringUtils;
38  import org.apache.commons.lang3.time.StopWatch;
39  import org.jspecify.annotations.Nullable;
40  import org.slf4j.Logger;
41  import org.slf4j.LoggerFactory;
42  import ubic.basecode.ontology.model.OntologyIndividual;
43  import ubic.basecode.ontology.model.OntologyModel;
44  import ubic.basecode.ontology.model.OntologyResource;
45  import ubic.basecode.ontology.model.OntologyTerm;
46  import ubic.basecode.ontology.providers.OntologyService;
47  import ubic.basecode.ontology.search.OntologySearchException;
48  import ubic.basecode.ontology.search.OntologySearchResult;
49  
50  import java.io.IOException;
51  import java.io.InputStream;
52  import java.io.InterruptedIOException;
53  import java.nio.channels.ClosedByInterruptException;
54  import java.util.*;
55  import java.util.function.Predicate;
56  import java.util.stream.Collectors;
57  
58  import static ubic.basecode.ontology.jena.JenaUtils.as;
59  
60  public abstract class AbstractOntologyService implements OntologyService {
61  
62      /**
63       * Properties through which propagation is allowed for {@link #getParents(Collection, boolean, boolean)}}
64       */
65      private static final Set<Property> DEFAULT_ADDITIONAL_PROPERTIES;
66      protected static Logger log = LoggerFactory.getLogger( AbstractOntologyService.class );
67  
68      static {
69          DEFAULT_ADDITIONAL_PROPERTIES = new HashSet<>();
70          DEFAULT_ADDITIONAL_PROPERTIES.add( RO.partOf );
71          // all those are sub-properties of partOf, but some ontologies might not have them
72          DEFAULT_ADDITIONAL_PROPERTIES.add( RO.activeIngredientIn );
73          DEFAULT_ADDITIONAL_PROPERTIES.add( RO.boundingLayerOf );
74          DEFAULT_ADDITIONAL_PROPERTIES.add( RO.branchingPartOf );
75          DEFAULT_ADDITIONAL_PROPERTIES.add( RO.determinedBy );
76          DEFAULT_ADDITIONAL_PROPERTIES.add( RO.ends );
77          DEFAULT_ADDITIONAL_PROPERTIES.add( RO.isSubsequenceOf );
78          DEFAULT_ADDITIONAL_PROPERTIES.add( RO.isEndSequenceOf );
79          DEFAULT_ADDITIONAL_PROPERTIES.add( RO.isStartSequenceOf );
80          DEFAULT_ADDITIONAL_PROPERTIES.add( RO.lumenOf );
81          DEFAULT_ADDITIONAL_PROPERTIES.add( RO.luminalSpaceOf );
82          DEFAULT_ADDITIONAL_PROPERTIES.add( RO.mainStemOf );
83          DEFAULT_ADDITIONAL_PROPERTIES.add( RO.memberOf );
84          DEFAULT_ADDITIONAL_PROPERTIES.add( RO.occurrentPartOf );
85          DEFAULT_ADDITIONAL_PROPERTIES.add( RO.skeletonOf );
86          DEFAULT_ADDITIONAL_PROPERTIES.add( RO.starts );
87          DEFAULT_ADDITIONAL_PROPERTIES.add( RO.subclusterOf );
88          // used by some older ontologies
89          //noinspection deprecation
90          DEFAULT_ADDITIONAL_PROPERTIES.add( RO.properPartOf );
91      }
92  
93      private final String ontologyName;
94      private final String ontologyUrl;
95      private final boolean ontologyEnabled;
96      @Nullable
97      private final String cacheName;
98  
99      /**
100      * Internal state.
101      */
102     @Nullable
103     private State state = null;
104     /* settings (applicable for next initialization) */
105     private LanguageLevel languageLevel = LanguageLevel.FULL;
106     private InferenceMode inferenceMode = InferenceMode.TRANSITIVE;
107     private boolean processImports = true;
108     private boolean searchEnabled = true;
109     private Set<String> excludedWordsFromStemming = Collections.emptySet();
110     private Set<String> additionalPropertyUris = DEFAULT_ADDITIONAL_PROPERTIES.stream().map( Property::getURI ).collect( Collectors.toSet() );
111 
112     protected AbstractOntologyService( String ontologyName, String ontologyUrl, boolean ontologyEnabled, @Nullable String cacheName ) {
113         this.ontologyName = ontologyName;
114         this.ontologyUrl = ontologyUrl;
115         this.ontologyEnabled = ontologyEnabled;
116         this.cacheName = cacheName;
117     }
118 
119     protected String getOntologyName() {
120         return ontologyName;
121     }
122 
123     protected String getOntologyUrl() {
124         return ontologyUrl;
125     }
126 
127     protected boolean isOntologyEnabled() {
128         return ontologyEnabled;
129     }
130 
131     @Nullable
132     protected String getCacheName() {
133         return cacheName;
134     }
135 
136     @Nullable
137     private volatile Thread initializationThread = null;
138 
139     @Override
140     public synchronized void startInitializationThread( boolean forceLoad, boolean forceIndexing ) {
141         Thread initializationThread = this.initializationThread;
142         if ( initializationThread != null && initializationThread.isAlive() ) {
143             log.warn( " Initialization thread for {} is currently running, not restarting.", this );
144             return;
145         }
146         // create and start the initialization thread
147         initializationThread = new Thread( () -> {
148             try {
149                 this.initialize( forceLoad, forceIndexing );
150             } catch ( Exception e ) {
151                 log.error( "Initialization for %s failed.", e );
152             }
153         }, getOntologyName() + "_load_thread_" + RandomStringUtils.insecure().nextAlphanumeric( 5 ) );
154         // To prevent VM from waiting on this thread to shut down (if shutting down).
155         initializationThread.setDaemon( true );
156         initializationThread.start();
157         this.initializationThread = initializationThread;
158     }
159 
160     @Override
161     public boolean isInitializationThreadAlive() {
162         Thread initializationThread = this.initializationThread;
163         return initializationThread != null && initializationThread.isAlive();
164     }
165 
166     @Override
167     public boolean isInitializationThreadCancelled() {
168         Thread initializationThread = this.initializationThread;
169         return initializationThread != null && initializationThread.isInterrupted();
170     }
171 
172     /**
173      * Cancel the initialization thread.
174      */
175     @Override
176     public void cancelInitializationThread() {
177         Thread initializationThread = this.initializationThread;
178         if ( initializationThread == null ) {
179             throw new IllegalStateException( "The initialization thread has not started. Invoke startInitializationThread() first." );
180         }
181         initializationThread.interrupt();
182     }
183 
184     @Override
185     public void waitForInitializationThread() throws InterruptedException {
186         Thread initializationThread = this.initializationThread;
187         if ( initializationThread == null ) {
188             throw new IllegalStateException( "The initialization thread has not started. Invoke startInitializationThread() first." );
189         }
190         initializationThread.join();
191     }
192 
193     @Override
194     public void loadTermsInNameSpace( InputStream is, boolean forceIndex ) {
195         // wait for the initialization thread to finish
196         Thread initializationThread = this.initializationThread;
197         if ( initializationThread != null && initializationThread.isAlive() ) {
198             log.warn( "{} initialization is already running, trying to cancel ...", this );
199             initializationThread.interrupt();
200             // wait for the thread to die.
201             int maxWait = 10;
202             int wait = 0;
203             while ( initializationThread.isAlive() ) {
204                 try {
205                     initializationThread.join( 5000 );
206                 } catch ( InterruptedException e ) {
207                     Thread.currentThread().interrupt();
208                     return;
209                 }
210                 log.warn( "Waiting for auto-initialization to stop so manual initialization can begin ..." );
211                 ++wait;
212                 if ( wait >= maxWait && !initializationThread.isAlive() ) {
213                     throw new RuntimeException( String.format( "Got tired of waiting for %s's initialization thread.", this ) );
214                 }
215             }
216         }
217         initialize( is, forceIndex );
218     }
219 
220     @Nullable
221     @Override
222     public String getName() {
223         return getState().map( state -> {
224             NodeIterator it = state.model.listObjectsOfProperty( DC_11.title );
225             return it.hasNext() ? it.next().asLiteral().getString() : null;
226         } ).orElse( null );
227     }
228 
229     @Nullable
230     @Override
231     public String getDescription() {
232         return getState().map( state -> {
233             NodeIterator it = state.model.listObjectsOfProperty( DC_11.description );
234             return it.hasNext() ? it.next().asLiteral().getString() : null;
235         } ).orElse( null );
236     }
237 
238     @Override
239     public LanguageLevel getLanguageLevel() {
240         return getState().map( state -> state.languageLevel ).orElse( languageLevel );
241     }
242 
243     @Override
244     public void setLanguageLevel( LanguageLevel languageLevel ) {
245         this.languageLevel = languageLevel;
246     }
247 
248     @Override
249     public InferenceMode getInferenceMode() {
250         return getState().map( state -> state.inferenceMode ).orElse( inferenceMode );
251     }
252 
253     @Override
254     public void setInferenceMode( InferenceMode inferenceMode ) {
255         this.inferenceMode = inferenceMode;
256     }
257 
258     @Override
259     public boolean getProcessImports() {
260         return getState().map( state -> state.processImports ).orElse( processImports );
261     }
262 
263     @Override
264     public void setProcessImports( boolean processImports ) {
265         this.processImports = processImports;
266     }
267 
268     @Override
269     public boolean isSearchEnabled() {
270         return getState().map( state -> state.index != null ).orElse( searchEnabled );
271     }
272 
273     @Override
274     public void setSearchEnabled( boolean searchEnabled ) {
275         this.searchEnabled = searchEnabled;
276     }
277 
278     @Override
279     public Set<String> getExcludedWordsFromStemming() {
280         return getState().map( state -> state.excludedWordsFromStemming ).orElse( excludedWordsFromStemming );
281     }
282 
283     @Override
284     public void setExcludedWordsFromStemming( Set<String> excludedWordsFromStemming ) {
285         this.excludedWordsFromStemming = excludedWordsFromStemming;
286     }
287 
288     @Override
289     public Set<String> getAdditionalPropertyUris() {
290         return getState().map( state -> state.additionalPropertyUris ).orElse( additionalPropertyUris );
291     }
292 
293     @Override
294     public void setAdditionalPropertyUris( Set<String> additionalPropertyUris ) {
295         this.additionalPropertyUris = additionalPropertyUris;
296     }
297 
298     public void initialize( boolean forceLoad, boolean forceIndexing ) {
299         initialize( null, forceLoad, forceIndexing );
300     }
301 
302     public void initialize( InputStream stream, boolean forceIndexing ) {
303         initialize( stream, true, forceIndexing );
304     }
305 
306     private synchronized void initialize( @Nullable InputStream stream, boolean forceLoad, boolean forceIndexing ) {
307         if ( !forceLoad && state != null ) {
308             log.warn( "{} is already loaded, and force=false, not restarting", this );
309             return;
310         }
311 
312         // making a copy of all we need
313         String ontologyUrl = getOntologyUrl();
314         String ontologyName = getOntologyName();
315         String cacheName = getCacheName();
316         LanguageLevel languageLevel = this.languageLevel;
317         InferenceMode inferenceMode = this.inferenceMode;
318         boolean processImports = this.processImports;
319         boolean searchEnabled = this.searchEnabled;
320         Set<String> excludedWordsFromStemming = this.excludedWordsFromStemming;
321 
322         // Detect configuration problems.
323         if ( StringUtils.isBlank( ontologyUrl ) ) {
324             throw new IllegalStateException( "URL not defined for %s: ontology cannot be loaded. (" + this + ")" );
325         }
326 
327         if ( cacheName == null && forceIndexing ) {
328             throw new IllegalArgumentException( String.format( "No cache directory is set for %s, cannot force indexing.", this ) );
329         }
330 
331         boolean loadOntology = isEnabled();
332 
333         // If loading ontologies is disabled in the configuration, return
334         if ( !forceLoad && !loadOntology ) {
335             log.debug( "Loading {} is disabled (force=false, Configuration load.{}=false)",
336                     this, ontologyName );
337             return;
338         }
339 
340         log.info( "Loading ontology: {}...", this );
341         StopWatch loadTime = StopWatch.createStarted();
342 
343         // use temporary variables, so that we can minimize the critical region for replacing the service's state
344         OntModel model;
345         SearchIndex index;
346 
347         // loading the model from disk or URL is lengthy
348         if ( Thread.currentThread().isInterrupted() )
349             return;
350 
351         try {
352             OntologyModel m = stream != null ? loadModelFromStream( stream, processImports, languageLevel, inferenceMode ) : loadModel( processImports, languageLevel, inferenceMode ); // can take a while.
353             model = m.unwrap( OntModel.class );
354         } catch ( Exception e ) {
355             if ( isCausedByInterrupt( e ) ) {
356                 // make sure that the thread is interrupted
357                 Thread.currentThread().interrupt();
358                 return;
359             } else {
360                 throw new RuntimeException( String.format( "Failed to load ontology model for %s.", this ), e );
361             }
362         }
363 
364         // retrieving restrictions is lengthy
365         if ( Thread.currentThread().isInterrupted() )
366             return;
367 
368         // compute additional restrictions
369         Set<Property> additionalProperties = additionalPropertyUris.stream().map( model::getProperty ).collect( Collectors.toSet() );
370         Set<Restriction> additionalRestrictions = JenaUtils.listRestrictionsOnProperties( model, additionalProperties, true ).toSet();
371 
372 
373         // indexing is lengthy, don't bother if we're interrupted
374         if ( Thread.currentThread().isInterrupted() )
375             return;
376 
377         if ( searchEnabled && cacheName != null ) {
378             //Checks if the current ontology has changed since it was last loaded.
379             boolean changed = OntologyLoader.hasChanged( cacheName );
380             boolean indexExists = OntologyIndexer.getSubjectIndex( cacheName, excludedWordsFromStemming ) != null;
381             boolean forceReindexing = forceLoad && forceIndexing;
382             // indexing is slow, don't do it if we don't have to.
383             try {
384                 index = OntologyIndexer.indexOntology( cacheName, model, excludedWordsFromStemming, forceReindexing || changed || !indexExists );
385             } catch ( Exception e ) {
386                 if ( isCausedByInterrupt( e ) ) {
387                     return;
388                 } else {
389                     throw new RuntimeException( String.format( "Failed to generate index for %s.", this ), e );
390                 }
391             }
392         } else {
393             index = null;
394         }
395 
396         // if interrupted, we don't need to replace the model and clear the *old* cache
397         if ( Thread.currentThread().isInterrupted() )
398             return;
399 
400         if ( this.state != null ) {
401             try {
402                 this.state.close();
403             } catch ( Exception e ) {
404                 log.error( "Failed to close current state.", e );
405             }
406         }
407 
408         this.state = new State( model, index, excludedWordsFromStemming, additionalRestrictions, languageLevel, inferenceMode, processImports, additionalProperties.stream().map( Property::getURI ).collect( Collectors.toSet() ), null );
409         if ( cacheName != null ) {
410             // now that the terms have been replaced, we can clear old caches
411             try {
412                 OntologyLoader.deleteOldCache( cacheName );
413             } catch ( IOException e ) {
414                 log.error( String.format( String.format( "Failed to delete old cache directory for %s.", this ), e ) );
415             }
416         }
417 
418         loadTime.stop();
419 
420         log.info( "Finished loading {} in {}s", this, String.format( "%.2f", loadTime.getTime() / 1000.0 ) );
421     }
422 
423     private boolean isCausedByInterrupt( Exception e ) {
424         return hasCauseMatching( e, cause -> ( ( cause instanceof ParseException ) && ( ( ParseException ) cause ).getErrorNumber() == ARPErrorNumbers.ERR_INTERRUPTED ) ) ||
425                 hasCause( e, InterruptedException.class ) ||
426                 hasCause( e, InterruptedIOException.class ) ||
427                 hasCause( e, ClosedByInterruptException.class );
428     }
429 
430     private boolean hasCause( Throwable t, Class<? extends Throwable> clazz ) {
431         return hasCauseMatching( t, clazz::isInstance );
432     }
433 
434     private boolean hasCauseMatching( Throwable t, Predicate<Throwable> predicate ) {
435         return predicate.test( t ) || ( t.getCause() != null && hasCauseMatching( t.getCause(), predicate ) );
436     }
437 
438     @Override
439     public Collection<OntologySearchResult<OntologyIndividual>> findIndividuals( String search, int maxResults, boolean keepObsoletes ) throws
440             OntologySearchException {
441         State state = this.state;
442         if ( state == null ) {
443             log.warn( "Ontology {} is not ready, no individuals will be returned.", this );
444             return Collections.emptySet();
445         }
446         if ( state.index == null ) {
447             log.warn( "Attempt to search {} when index is null, no results will be returned.", this );
448             return Collections.emptySet();
449         }
450         return state.index.searchIndividuals( state.model, search, maxResults ).stream()
451                 .map( i -> as( i.result, Individual.class ).map( r -> new OntologySearchResult<>( ( OntologyIndividual ) new OntologyIndividualImpl( r, state.additionalRestrictions ), i.score ) ) )
452                 .filter( Optional::isPresent )
453                 .map( Optional::get )
454                 .filter( ontologyTerm -> keepObsoletes || !ontologyTerm.getResult().isObsolete() )
455                 .sorted( Comparator.comparing( OntologySearchResult::getScore, Comparator.reverseOrder() ) )
456                 .collect( Collectors.toCollection( LinkedHashSet::new ) );
457     }
458 
459     @Override
460     public Collection<OntologySearchResult<OntologyResource>> findResources( String searchString, int maxResults, boolean keepObsoletes ) throws
461             OntologySearchException {
462         State state = this.state;
463         if ( state == null ) {
464             log.warn( "Ontology {} is not ready, no resources will be returned.", this );
465             return Collections.emptySet();
466         }
467         if ( state.index == null ) {
468             log.warn( "Attempt to search {} when index is null, no results will be returned.", this );
469             return Collections.emptySet();
470         }
471         return state.index.search( state.model, searchString, maxResults ).stream()
472                 .filter( ( r -> r.result.canAs( OntClass.class ) || r.result.canAs( Individual.class ) ) )
473                 .map( r -> {
474                     if ( r.result.canAs( OntClass.class ) ) {
475                         return as( r.result, OntClass.class )
476                                 .map( r2 -> new OntologySearchResult<>( ( OntologyResource ) new OntologyTermImpl( r2, state.additionalRestrictions ), r.score ) );
477                     } else if ( r.result.canAs( Individual.class ) ) {
478                         return as( r.result, Individual.class )
479                                 .map( r2 -> new OntologySearchResult<>( ( OntologyResource ) new OntologyIndividualImpl( r2, state.additionalRestrictions ), r.score ) );
480                     } else {
481                         return Optional.<OntologySearchResult<OntologyResource>>empty();
482                     }
483                 } )
484                 .filter( Optional::isPresent )
485                 .map( Optional::get )
486                 .filter( ontologyTerm -> keepObsoletes || !ontologyTerm.getResult().isObsolete() )
487                 .sorted( Comparator.comparing( OntologySearchResult::getScore, Comparator.reverseOrder() ) )
488                 .collect( Collectors.toCollection( LinkedHashSet::new ) );
489     }
490 
491     @Override
492     public Collection<OntologySearchResult<OntologyTerm>> findTerm( String search, int maxResults, boolean keepObsoletes ) throws OntologySearchException {
493         State state = this.state;
494         if ( state == null ) {
495             log.warn( "Ontology {} is not ready, no terms will be returned.", this );
496             return Collections.emptySet();
497         }
498         if ( state.index == null ) {
499             log.warn( "Attempt to search {} when index is null, no results will be returned.", this );
500             return Collections.emptySet();
501         }
502         return state.index.searchClasses( state.model, search, maxResults ).stream()
503                 .map( r -> as( r.result, OntClass.class ).map( s -> new OntologySearchResult<>( ( OntologyTerm ) new OntologyTermImpl( s, state.additionalRestrictions ), r.score ) ) )
504                 .filter( Optional::isPresent )
505                 .map( Optional::get )
506                 .filter( ontologyTerm -> keepObsoletes || !ontologyTerm.getResult().isObsolete() )
507                 .sorted( Comparator.comparing( OntologySearchResult::getScore, Comparator.reverseOrder() ) )
508                 .collect( Collectors.toCollection( LinkedHashSet::new ) );
509     }
510 
511     @Nullable
512     @Override
513     public OntologyTerm findUsingAlternativeId( String alternativeId ) {
514         State state = this.state;
515         if ( state == null ) {
516             log.warn( "Ontology {} is not ready, null will be returned for alternative ID match.", this );
517             return null;
518         }
519         if ( state.alternativeIDs == null ) {
520             log.info( "init search by alternativeID" );
521             this.state = initSearchByAlternativeId( state );
522         }
523         assert state.alternativeIDs != null;
524         String termUri = state.alternativeIDs.get( alternativeId );
525         return termUri != null ? getTerm( termUri ) : null;
526     }
527 
528     @Override
529     public Set<String> getAllURIs() {
530         return getState().map( state -> {
531             Set<String> allUris = new HashSet<>();
532             allUris.addAll( state.model.listClasses().mapWith( OntClass::getURI ).toSet() );
533             allUris.addAll( state.model.listIndividuals().mapWith( Individual::getURI ).toSet() );
534             return allUris;
535         } ).orElseGet( () -> {
536             log.warn( "Ontology {} is not ready, no term  URIs will be returned.", this );
537             return Collections.emptySet();
538         } );
539     }
540 
541     @Nullable
542     @Override
543     public OntologyResource getResource( String uri ) {
544         return getState().map( state -> {
545             OntologyResource res;
546             Resource resource = state.model.getResource( uri );
547             if ( resource.getURI() == null ) {
548                 return null;
549             }
550             if ( resource instanceof OntClass ) {
551                 // use the cached term
552                 res = new OntologyTermImpl( ( OntClass ) resource, state.additionalRestrictions );
553             } else if ( resource instanceof Individual ) {
554                 res = new OntologyIndividualImpl( ( Individual ) resource, state.additionalRestrictions );
555             } else if ( resource instanceof OntProperty ) {
556                 res = PropertyFactory.asProperty( ( ObjectProperty ) resource, state.additionalRestrictions );
557             } else {
558                 res = null;
559             }
560             return res;
561         } ).orElse( null );
562     }
563 
564     @Nullable
565     @Override
566     public OntologyTerm getTerm( String uri ) {
567         return getState().map( state -> {
568             OntClass ontCls = state.model.getOntClass( uri );
569             // null or bnode
570             if ( ontCls == null || ontCls.getURI() == null ) {
571                 return null;
572             }
573             return new OntologyTermImpl( ontCls, state.additionalRestrictions );
574         } ).orElse( null );
575     }
576 
577     @Override
578     public Collection<OntologyIndividual> getTermIndividuals( String uri ) {
579         OntologyTerm term = getTerm( uri );
580         if ( term == null ) {
581             /*
582              * Either the ontology hasn't been loaded, or the id was not valid.
583              */
584             log.warn( "No term for URI={} in {}; make sure ontology is loaded and uri is valid", uri, this );
585             return Collections.emptySet();
586         }
587         return term.getIndividuals( true );
588     }
589 
590     @Override
591     public Set<OntologyTerm> getParents( Collection<OntologyTerm> terms, boolean direct,
592             boolean includeAdditionalProperties, boolean keepObsoletes ) {
593         return getState().map( state ->
594                         JenaUtils.getParents( state.model, getOntClassesFromTerms( state.model, terms ), direct, includeAdditionalProperties ? state.additionalRestrictions : null )
595                                 .stream()
596                                 .map( o -> ( OntologyTerm ) new OntologyTermImpl( o, state.additionalRestrictions ) )
597                                 .filter( o -> keepObsoletes || !o.isObsolete() )
598                                 .collect( Collectors.toSet() ) )
599                 .orElse( Collections.emptySet() );
600     }
601 
602     @Override
603     public Set<OntologyTerm> getChildren( Collection<OntologyTerm> terms, boolean direct,
604             boolean includeAdditionalProperties, boolean keepObsoletes ) {
605         return getState().map( state ->
606                 JenaUtils.getChildren( state.model, getOntClassesFromTerms( state.model, terms ), direct, includeAdditionalProperties ? state.additionalRestrictions : null )
607                         .stream()
608                         .map( o -> ( OntologyTerm ) new OntologyTermImpl( o, state.additionalRestrictions ) )
609                         .filter( o -> keepObsoletes || !o.isObsolete() )
610                         .collect( Collectors.toSet() )
611         ).orElse( Collections.emptySet() );
612     }
613 
614     @Override
615     public boolean isEnabled() {
616         // could have forced, without setting config
617         return isOntologyEnabled() || isOntologyLoaded() || isInitializationThreadAlive();
618     }
619 
620     @Override
621     public boolean isOntologyLoaded() {
622         // it's fine not to use the read lock here
623         return state != null;
624     }
625 
626     /**
627      * Delegates the call as to load the model into memory or leave it on disk. Simply delegates to either
628      * OntologyLoader.loadMemoryModel( url ); OR OntologyLoader.loadPersistentModel( url, spec );
629      */
630     protected abstract OntologyModel loadModel( boolean processImports, LanguageLevel languageLevel, InferenceMode inferenceMode ) throws IOException;
631 
632     /**
633      * Load a model from a given input stream.
634      */
635     protected abstract OntologyModel loadModelFromStream( InputStream is, boolean processImports, LanguageLevel languageLevel, InferenceMode inferenceMode ) throws IOException;
636 
637     protected OntModelSpec getSpec( LanguageLevel languageLevel, InferenceMode inferenceMode ) {
638         String profile;
639         switch ( languageLevel ) {
640             case FULL:
641                 profile = ProfileRegistry.OWL_LANG;
642                 break;
643             case DL:
644                 profile = ProfileRegistry.OWL_DL_LANG;
645                 break;
646             case LITE:
647                 profile = ProfileRegistry.OWL_LITE_LANG;
648                 break;
649             default:
650                 throw new UnsupportedOperationException( String.format( "Unsupported OWL language level %s.", languageLevel ) );
651         }
652         ReasonerFactory reasonerFactory;
653         switch ( inferenceMode ) {
654             case FULL:
655                 reasonerFactory = OWLFBRuleReasonerFactory.theInstance();
656                 break;
657             case MINI:
658                 reasonerFactory = OWLMiniReasonerFactory.theInstance();
659                 break;
660             case MICRO:
661                 reasonerFactory = OWLMicroReasonerFactory.theInstance();
662                 break;
663             case TRANSITIVE:
664                 reasonerFactory = TransitiveReasonerFactory.theInstance();
665                 break;
666             case NONE:
667                 reasonerFactory = null;
668                 break;
669             default:
670                 throw new UnsupportedOperationException( String.format( "Unsupported inference level %s.", inferenceMode ) );
671         }
672         return new OntModelSpec( ModelFactory.createMemModelMaker(), null, reasonerFactory, profile );
673     }
674 
675     @Override
676     public synchronized void index( boolean force ) {
677         String cacheName = getCacheName();
678         if ( cacheName == null ) {
679             log.warn( "This ontology does not support indexing; assign a cache name to be used." );
680             return;
681         }
682         if ( !searchEnabled ) {
683             log.warn( "Search is not enabled for this ontology." );
684             return;
685         }
686         State state = this.state;
687         if ( state == null ) {
688             log.warn( "Ontology {} is not initialized, cannot index it.", this );
689             return;
690         }
691         SearchIndex index;
692         try {
693             index = OntologyIndexer.indexOntology( cacheName, state.model, state.excludedWordsFromStemming, force );
694         } catch ( IOException e ) {
695             log.error( "Failed to generate index for {}.", this, e );
696             return;
697         }
698         // now we replace the index
699         this.state = new State( state.model, index, state.excludedWordsFromStemming, state.additionalRestrictions, state.languageLevel, state.inferenceMode, state.processImports, state.additionalPropertyUris, state.alternativeIDs );
700     }
701 
702     /**
703      * Initialize alternative IDs mapping.
704      * <p>
705      * this add alternative id in 2 ways
706      * <p>
707      * Example :
708      * <p>
709      * <a href="http://purl.obolibrary.org/obo/HP_0000005">HP_0000005</a> with alternative id : HP:0001453
710      * <p>
711      * by default way use in file 1- HP:0001453 -----> <a href="http://purl.obolibrary.org/obo/HP_0000005">HP_0000005</a>
712      * <p>
713      * trying <a href=" to use the value uri 2- http://purl.obol">HP_0001453</a>ibrary.org/obo/HP_0001453 ----->
714      * <a href="http://purl.obolibrary.org/obo/HP_0000005">HP_0000005</a>
715      */
716     private State initSearchByAlternativeId( State state ) {
717         Map<String, String> alternativeIDs = new HashMap<>();
718         // for all Ontology terms that exist in the tree
719         ExtendedIterator<OntClass> iterator = state.model.listClasses();
720         while ( iterator.hasNext() ) {
721             OntologyTerm ontologyTerm = new OntologyTermImpl( iterator.next(), state.additionalRestrictions );
722             if ( ontologyTerm.getUri() == null ) {
723                 continue;
724             }
725             // let's find the baseUri, to change to valueUri
726             String baseOntologyUri = ontologyTerm.getUri().substring( 0, ontologyTerm.getUri().lastIndexOf( "/" ) + 1 );
727             for ( String alternativeId : ontologyTerm.getAlternativeIds() ) {
728                 // first way
729                 alternativeIDs.put( alternativeId, ontologyTerm.getUri() );
730                 // second way
731                 String alternativeIdModified = alternativeId.replace( ':', '_' );
732                 alternativeIDs.put( baseOntologyUri + alternativeIdModified, ontologyTerm.getUri() );
733             }
734         }
735         return new State( state.model, state.index, state.excludedWordsFromStemming, state.additionalRestrictions, state.languageLevel, state.inferenceMode, state.processImports, state.additionalPropertyUris, alternativeIDs );
736     }
737 
738     @Override
739     public void close() throws Exception {
740         if ( state != null ) {
741             state.close();
742         }
743     }
744 
745     @Override
746     public String toString() {
747         return String.format( "%s [url=%s] [language level=%s] [inference mode=%s] [imports=%b] [search=%b]",
748                 getOntologyName(), getOntologyUrl(), getLanguageLevel(), getInferenceMode(), getProcessImports(), isSearchEnabled() );
749     }
750 
751     private Optional<State> getState() {
752         return Optional.ofNullable( state );
753     }
754 
755     private Set<OntClass> getOntClassesFromTerms( OntModel model, Collection<OntologyTerm> terms ) {
756         return terms.stream()
757                 .map( o -> {
758                     if ( o instanceof OntologyTermImpl ) {
759                         return ( ( OntologyTermImpl ) o ).getOntClass();
760                     } else {
761                         return o.getUri() != null ? model.getOntClass( o.getUri() ) : null;
762                     }
763                 } )
764                 .filter( Objects::nonNull )
765                 .collect( Collectors.toSet() );
766     }
767 
768     private static class State implements AutoCloseable {
769         private final OntModel model;
770         @Nullable
771         private final SearchIndex index;
772         private final Set<String> excludedWordsFromStemming;
773         private final Set<Restriction> additionalRestrictions;
774         @Nullable
775         private final LanguageLevel languageLevel;
776         private final InferenceMode inferenceMode;
777         private final boolean processImports;
778         private final Set<String> additionalPropertyUris;
779         @Nullable
780         private final Map<String, String> alternativeIDs;
781 
782         private State( OntModel model, @Nullable SearchIndex index, Set<String> excludedWordsFromStemming, Set<Restriction> additionalRestrictions, @Nullable LanguageLevel languageLevel, InferenceMode inferenceMode, boolean processImports, Set<String> additionalPropertyUris, @Nullable Map<String, String> alternativeIDs ) {
783             this.model = model;
784             this.index = index;
785             this.excludedWordsFromStemming = excludedWordsFromStemming;
786             this.additionalRestrictions = additionalRestrictions;
787             this.languageLevel = languageLevel;
788             this.inferenceMode = inferenceMode;
789             this.processImports = processImports;
790             this.additionalPropertyUris = additionalPropertyUris;
791             this.alternativeIDs = alternativeIDs;
792         }
793 
794         @Override
795         public void close() throws Exception {
796             try {
797                 model.close();
798             } finally {
799                 if ( index != null ) {
800                     index.close();
801                 }
802             }
803         }
804     }
805 }