View Javadoc
1   package ubic.basecode.ontology.jena;
2   
3   import com.hp.hpl.jena.ontology.*;
4   import com.hp.hpl.jena.rdf.model.Property;
5   import com.hp.hpl.jena.rdf.model.RDFNode;
6   import com.hp.hpl.jena.rdf.model.Resource;
7   import com.hp.hpl.jena.rdf.model.ResourceFactory;
8   import com.hp.hpl.jena.util.iterator.ExtendedIterator;
9   import com.hp.hpl.jena.util.iterator.UniqueExtendedIterator;
10  import org.apache.commons.lang3.time.StopWatch;
11  import org.slf4j.Logger;
12  import org.slf4j.LoggerFactory;
13  
14  import org.jspecify.annotations.Nullable;
15  import java.util.*;
16  import java.util.stream.Collectors;
17  
18  import static com.hp.hpl.jena.reasoner.ReasonerRegistry.makeDirect;
19  
20  class JenaUtils {
21  
22      protected static final Logger log = LoggerFactory.getLogger( JenaUtils.class );
23  
24      /**
25       * Safely convert a {@link RDFNode} to a target class.
26       */
27      public static <T extends RDFNode> Optional<T> as( RDFNode resource, Class<T> clazz ) {
28          if ( !resource.canAs( clazz ) ) {
29              return Optional.empty();
30          }
31          try {
32              return Optional.of( resource.as( clazz ) );
33          } catch ( ConversionException e ) {
34              log.warn( "Conversion of {} to {} failed.", resource, clazz.getName() );
35              return Optional.empty();
36          }
37      }
38  
39      public static Collection<OntClass> getParents( OntModel model, Collection<OntClass> ontClasses, boolean direct, @Nullable Set<Restriction> additionalRestrictions ) {
40          Collection<OntClass> parents = getParentsInternal( model, ontClasses, direct, additionalRestrictions );
41          if ( shouldRevisit( parents, direct, model, additionalRestrictions ) ) {
42              // if there are some missing direct parents, revisit the hierarchy
43              Set<OntClass> parentsToRevisit = new HashSet<>( parents );
44              while ( !parentsToRevisit.isEmpty() ) {
45                  log.debug( "Revisiting the direct parents of {} terms...", parentsToRevisit.size() );
46                  parentsToRevisit = new HashSet<>( getParentsInternal( model, parentsToRevisit, true, additionalRestrictions ) );
47                  parentsToRevisit.removeAll( parents );
48                  log.debug( "Found {} missed parents.", parentsToRevisit.size() );
49                  parents.addAll( parentsToRevisit );
50              }
51          }
52          return parents;
53      }
54  
55      private static Collection<OntClass> getParentsInternal( OntModel model, Collection<OntClass> ontClasses, boolean direct, @Nullable Set<Restriction> additionalRestrictions ) {
56          ontClasses = ontClasses.stream()
57              .map( t -> t.inModel( model ) )
58              .filter( t -> t.canAs( OntClass.class ) )
59              .map( t -> as( t, OntClass.class ) )
60              .filter( Optional::isPresent )
61              .map( Optional::get )
62              .collect( Collectors.toSet() );
63          if ( ontClasses.isEmpty() ) {
64              return Collections.emptySet();
65          }
66          Iterator<OntClass> it = ontClasses.iterator();
67          ExtendedIterator<OntClass> iterator = it.next().listSuperClasses( direct );
68          while ( it.hasNext() ) {
69              iterator = iterator.andThen( it.next().listSuperClasses( direct ) );
70          }
71  
72          Collection<OntClass> result = new HashSet<>();
73          while ( iterator.hasNext() ) {
74              OntClass c = iterator.next();
75  
76              // handles part of some {parent container} or part of all {parent container}
77              if ( additionalRestrictions != null && !additionalRestrictions.isEmpty() && c.isRestriction() ) {
78                  Restriction r = c.asRestriction();
79                  if ( additionalRestrictions.contains( r ) ) {
80                      Resource value = getRestrictionValue( r );
81                      if ( value instanceof OntClass ) {
82                          c = ( OntClass ) value;
83                      } else {
84                          continue;
85                      }
86                  }
87              }
88  
89              // bnode
90              if ( c.getURI() == null )
91                  continue;
92  
93              // owl:Thing
94              if ( c.equals( model.getProfile().THING() ) )
95                  continue;
96  
97              result.add( c );
98          }
99  
100         return result;
101     }
102 
103     public static Collection<OntClass> getChildren( OntModel model, Collection<OntClass> terms, boolean direct, @Nullable Set<Restriction> additionalRestrictions ) {
104         Collection<OntClass> children = getChildrenInternal( model, terms, direct, additionalRestrictions );
105         if ( shouldRevisit( children, direct, model, additionalRestrictions ) ) {
106             // if there are some missing direct children, revisit the hierarchy
107             Set<OntClass> childrenToRevisit = new HashSet<>( children );
108             while ( !childrenToRevisit.isEmpty() ) {
109                 log.debug( "Revisiting the direct parents of {} terms...", childrenToRevisit.size() );
110                 childrenToRevisit = new HashSet<>( JenaUtils.getChildrenInternal( model, childrenToRevisit, true, additionalRestrictions ) );
111                 childrenToRevisit.removeAll( children );
112                 log.debug( "Found {} missed children.", childrenToRevisit.size() );
113                 children.addAll( childrenToRevisit );
114             }
115         }
116         return children;
117     }
118 
119     public static Collection<OntClass> getChildrenInternal( OntModel model, Collection<OntClass> terms, boolean direct, @Nullable Set<Restriction> additionalRestrictions ) {
120         Set<OntClass> termsSet = terms.stream()
121             .map( t -> t.inModel( model ) )
122             .filter( t -> t.canAs( OntClass.class ) )
123             .map( t -> as( t, OntClass.class ) )
124             .filter( Optional::isPresent )
125             .map( Optional::get )
126             .collect( Collectors.toSet() );
127         if ( termsSet.isEmpty() ) {
128             return Collections.emptySet();
129         }
130         StopWatch timer = StopWatch.createStarted();
131         Iterator<OntClass> it = termsSet.iterator();
132         ExtendedIterator<OntClass> iterator = it.next().listSubClasses( direct );
133         while ( it.hasNext() ) {
134             iterator = iterator.andThen( it.next().listSubClasses( direct ) );
135         }
136         Set<OntClass> result = iterator
137             .filterDrop( new BnodeFilter<>() )
138             .filterDrop( new PredicateFilter<>( o -> o.equals( model.getProfile().NOTHING() ) ) )
139             .toSet();
140         if ( additionalRestrictions != null && !additionalRestrictions.isEmpty() ) {
141             timer.reset();
142             timer.start();
143             Property subClassOf = model.getProfile().SUB_CLASS_OF();
144             if ( direct ) {
145                 subClassOf = ResourceFactory.createProperty( makeDirect( subClassOf.getURI() ) );
146             }
147             Set<Restriction> restrictions = UniqueExtendedIterator.create( additionalRestrictions.iterator() )
148                 .filterKeep( new RestrictionWithValuesFromFilter( termsSet ) )
149                 .toSet();
150             for ( Restriction r : restrictions ) {
151                 result.addAll( model.listResourcesWithProperty( subClassOf, r )
152                     .filterDrop( new BnodeFilter<>() )
153                     .mapWith( r2 -> as( r2, OntClass.class ) )
154                     .filterKeep( new PredicateFilter<Optional<OntClass>>( Optional::isPresent ) )
155                     .mapWith( Optional::get )
156                     .toSet() );
157             }
158         }
159         return result;
160     }
161 
162     /**
163      * Check if a set of terms should be revisited to find missing parents or children.
164      * <p>
165      * To be considered, the model must have a reasoner that lacks one of {@code rdfs:subClassOf}, {@code owl:subValuesFrom}
166      * or {@code owl:allValuesFrom} inference capabilities. If a model has no reasoner, revisiting is not desirable and
167      * thus this will return false.
168      * <p>
169      * If direct is false or terms is empty, it's not worth revisiting.
170      */
171     private static boolean shouldRevisit( Collection<OntClass> terms, boolean direct, OntModel model, @Nullable Set<Restriction> additionalRestrictions ) {
172         return !direct
173             && !terms.isEmpty()
174             && additionalRestrictions != null
175             && !additionalRestrictions.isEmpty()
176             && model.getReasoner() != null
177             && ( !supportsSubClassInference( model ) || !supportsAdditionalRestrictionsInference( model ) );
178     }
179 
180     /**
181      * Check if an ontology model supports inference of {@code rdfs:subClassOf}.
182      */
183     public static boolean supportsSubClassInference( OntModel model ) {
184         return model.getReasoner() != null
185             && model.getReasoner().supportsProperty( model.getProfile().SUB_CLASS_OF() );
186     }
187 
188     /**
189      * Checks if an ontology model supports inference of with additional restrictions.
190      * <p>
191      * This covers {@code owl:subValuesFrom} and {@code owl:allValuesFrom} restrictions.
192      */
193     public static boolean supportsAdditionalRestrictionsInference( OntModel model ) {
194         return model.getReasoner() != null
195             && model.getReasoner().supportsProperty( model.getProfile().SOME_VALUES_FROM() )
196             && model.getReasoner().supportsProperty( model.getProfile().ALL_VALUES_FROM() );
197     }
198 
199     @Nullable
200     public static Resource getRestrictionValue( Restriction r ) {
201         if ( r.isSomeValuesFromRestriction() ) {
202             return r.asSomeValuesFromRestriction().getSomeValuesFrom();
203         } else if ( r.isAllValuesFromRestriction() ) {
204             return r.asAllValuesFromRestriction().getAllValuesFrom();
205         } else {
206             return null;
207         }
208     }
209 
210     /**
211      * List all restrictions in the given model on any of the given properties.
212      */
213     public static ExtendedIterator<Restriction> listRestrictionsOnProperties( OntModel model, Set<? extends Property> props, boolean includeSubProperties ) {
214         if ( includeSubProperties ) {
215             Set<Property> allProps = new HashSet<>( props );
216             for ( Property p : props ) {
217                 Property property = p.inModel( model );
218                 // include sub-properties for inference
219                 as( property, OntProperty.class ).ifPresent( op -> {
220                     ExtendedIterator<? extends OntProperty> it = op.listSubProperties( false );
221                     while ( it.hasNext() ) {
222                         OntProperty sp = it.next();
223                         allProps.add( sp );
224                     }
225                 } );
226             }
227             props = allProps;
228         }
229         return model.listRestrictions().filterKeep( new RestrictionWithOnPropertyFilter( props ) );
230     }
231 }