package net.enilink.komma.model; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.text.Collator; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.regex.Pattern; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.QualifiedName; import org.eclipse.core.runtime.content.IContentDescription; import org.eclipse.core.runtime.content.IContentType; import org.eclipse.core.runtime.content.IContentTypeManager; import org.eclipse.rdf4j.model.BNode; import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.model.Statement; import org.eclipse.rdf4j.model.ValueFactory; import org.eclipse.rdf4j.model.impl.SimpleValueFactory; import org.eclipse.rdf4j.model.vocabulary.OWL; import org.eclipse.rdf4j.model.vocabulary.RDF; import org.eclipse.rdf4j.rio.RDFFormat; import org.eclipse.rdf4j.rio.RDFHandler; import org.eclipse.rdf4j.rio.RDFHandlerException; import org.eclipse.rdf4j.rio.RDFParseException; import org.eclipse.rdf4j.rio.RDFParser; import org.eclipse.rdf4j.rio.RDFParserRegistry; import org.eclipse.rdf4j.rio.RDFWriter; import org.eclipse.rdf4j.rio.RDFWriterFactory; import org.eclipse.rdf4j.rio.RDFWriterRegistry; import org.eclipse.rdf4j.rio.Rio; import net.enilink.komma.common.util.BasicDiagnostic; import net.enilink.komma.common.util.Diagnostic; import net.enilink.komma.core.BlankNode; import net.enilink.komma.core.IEntity; import net.enilink.komma.core.ILiteral; import net.enilink.komma.core.INamespace; import net.enilink.komma.core.IReference; import net.enilink.komma.core.IStatement; import net.enilink.komma.core.KommaException; import net.enilink.komma.core.Namespace; import net.enilink.komma.core.URI; import net.enilink.komma.core.URIs; import net.enilink.komma.core.visitor.IDataAndNamespacesVisitor; import net.enilink.komma.core.visitor.IDataVisitor; import net.enilink.komma.rdf4j.RDF4JValueConverter; import net.enilink.vocab.rdf.Property; import net.enilink.vocab.rdfs.Class; import net.enilink.vocab.rdfs.Resource; public class ModelUtil { /** * Collator which can be used to compares resource labels. */ public static final Collator LABEL_COLLATOR; static { // add support for umlauts // in a multi-user environment (webapp) the locale // may need to be determined dynamically LABEL_COLLATOR = Collator.getInstance(Locale.GERMAN); LABEL_COLLATOR.setStrength(Collator.SECONDARY); } /** * Computes a {@link Diagnostic} from the errors and warnings stored in the * specified resource. * * @param model * @param includeWarnings * @return {@link Diagnostic} */ public static Diagnostic computeDiagnostic(IModel model, boolean includeWarnings) { if (model.getErrors().isEmpty() && (!includeWarnings || model.getWarnings().isEmpty())) { return Diagnostic.OK_INSTANCE; } else { BasicDiagnostic basicDiagnostic = new BasicDiagnostic(); for (IModel.IDiagnostic modelDiagnostic : model.getErrors()) { Diagnostic diagnostic = null; if (modelDiagnostic instanceof Throwable) { diagnostic = BasicDiagnostic.toDiagnostic( (Throwable) modelDiagnostic, model); } else { diagnostic = new BasicDiagnostic(Diagnostic.ERROR, ModelPlugin.PLUGIN_ID, 0, modelDiagnostic.getMessage(), new Object[] { model, modelDiagnostic }); } basicDiagnostic.add(diagnostic); } if (includeWarnings) { for (IModel.IDiagnostic modelDiagnostic : model.getWarnings()) { Diagnostic diagnostic = null; if (modelDiagnostic instanceof Throwable) { diagnostic = BasicDiagnostic.toDiagnostic( (Throwable) modelDiagnostic, model); } else { diagnostic = new BasicDiagnostic(Diagnostic.WARNING, ModelPlugin.PLUGIN_ID, 0, modelDiagnostic.getMessage(), new Object[] { model, modelDiagnostic }); } basicDiagnostic.add(diagnostic); } } return basicDiagnostic; } } /** * Compute a content description for a model URI. * * @param uriConverter * The URI converter which should be used * @param uri * The model URI * @return A content description or <code>null</code> * * @throws IOException * If an error occurred while compution the content description */ public static IContentDescription contentDescription( IURIConverter uriConverter, URI uri) throws IOException { String contentTypeId = (String) uriConverter.contentDescription(uri, null).get(IContentHandler.CONTENT_TYPE_PROPERTY); if (contentTypeId != null && Platform.getContentTypeManager() != null) { IContentType contentType = Platform.getContentTypeManager() .getContentType(contentTypeId); if (contentType != null) { return contentType.getDefaultDescription(); } } return null; } private static RDFWriter createWriter(OutputStream os, String baseURI, final String mimeType, String charset) throws IOException { RDFFormat format = RDFFormat.RDFXML; if (mimeType != null) { format = Rio.getParserFormatForMIMEType(mimeType).orElse(format); } // TODO: if necessary, re-implement RDF/XML pretty printing RDFWriterFactory factory = RDFWriterRegistry.getInstance().get(format) .orElseThrow(() -> new KommaException("No writer available for " + mimeType)); return factory.getWriter(os); } /** * Compute a content description for a model URI. * * @param uriConverter * The URI converter which should be used * @param uri * The model URI * @param options * Options for the URI converter * * @return A content description or <code>null</code> * @throws IOException * If an error occurred while compution the content description */ public static IContentDescription determineContentDescription(URI uri, IURIConverter uriConverter, Map<?, ?> options) throws IOException { IContentTypeManager contentTypeManager = Platform .getContentTypeManager(); if (contentTypeManager == null) { return null; } if (options == null) { options = Collections.emptyMap(); } IContentDescription contentDescription = null; try { String contentTypeId = (String) uriConverter.contentDescription( uri, options).get(IContentHandler.CONTENT_TYPE_PROPERTY); IContentType contentType = null; if (contentTypeId != null) { contentType = contentTypeManager.getContentType(contentTypeId); } if (contentType != null) { contentDescription = contentType.getDefaultDescription(); } } catch (IOException ioe) { // file does not exists } if (contentDescription == null) { // use file name with extension as fall back URI normalizedUri = uriConverter.normalize(uri); // simply use the filename to detect the correct RDF format String lastSegment = normalizedUri.lastSegment(); if (lastSegment != null) { IContentType[] matchingTypes = contentTypeManager .findContentTypesFor(lastSegment); for (IContentType matchingType : matchingTypes) { IContentDescription desc = matchingType .getDefaultDescription(); if (mimeType(desc) != null) { contentDescription = desc; break; } } } } if (contentDescription == null) { Map<Object, Object> optionsExt = new HashMap<>(options); optionsExt.put( IURIConverter.OPTION_REQUESTED_ATTRIBUTES, new HashSet<>(Arrays .asList(IURIConverter.ATTRIBUTE_MIME_TYPE))); // try to determine the Eclipse content-type based on the MIME-type Map<String, ?> attributes = uriConverter.getAttributes(uri, optionsExt); Object mimeType = attributes.get(IURIConverter.ATTRIBUTE_MIME_TYPE); if (mimeType != null) { for (IContentType contentType : contentTypeManager .getAllContentTypes()) { if (mimeType.equals(mimeType(contentType .getDefaultDescription()))) { contentDescription = contentType .getDefaultDescription(); break; } } } } return contentDescription; } private static RDFFormat determineFormat(String mimeType, InputStream in) { RDFFormat format = RDFFormat.RDFXML; if (mimeType != null) { format = Rio.getParserFormatForMIMEType(mimeType).orElse(format); } else if (in.markSupported()) { // try to distinguish RDF/XML and Turtle in.mark(2048); try { Reader r = new InputStreamReader(in, "UTF-8"); while (r.ready()) { int ch = r.read(); if (!Character.isWhitespace(ch) && // not the BOM character ch != 0xFEFF) { if (ch == '<') { if (r.ready() && (ch = r.read()) == '?') { // <?xml ...> // this is RDF/XML break; } StringBuilder tag = new StringBuilder(); tag.append((char) ch); int charsAfterColon = -1; while (r.ready() && (ch = r.read()) != '>') { tag.append((char) ch); if (charsAfterColon >= 0) { charsAfterColon++; } // read up to 4 chars after namespace separator // <ns:rdf [stop here] if (charsAfterColon > 3 || Character.isWhitespace(ch)) { break; } else if (ch == ':' && charsAfterColon < 0) { charsAfterColon = 0; } } // test for content like <ns:RDF ...> or <RDF ...> boolean isRdfXml = Pattern .compile("(^|[^:]+:)rdf\\s+$", Pattern.CASE_INSENSITIVE) .matcher(tag.toString()).matches(); if (isRdfXml) { break; } } format = RDFFormat.TURTLE; break; } } } catch (UnsupportedEncodingException e) { // should never happend } catch (IOException e) { throw new KommaException("Reading RDF data failed.", e); } try { in.reset(); } catch (IOException e) { throw new KommaException("Detection of RDF format failed."); } } return format; } /** * Returns a subset of the objects such that no object in the result is an * transitive container of any other object in the result. * * @param objects * the objects to be filtered. * @return a subset of the objects such that no object in the result is an * ancestor of any other object in the result. */ public static List<IObject> filterDescendants( Collection<? extends IObject> objects) { List<IObject> result = new ArrayList<IObject>(objects.size()); LOOP: for (IObject object : objects) { for (int i = 0, size = result.size(); i < size;) { IObject rootObject = result.get(i); if (rootObject.equals(object) || rootObject.getAllContents().contains(object)) { continue LOOP; } if (object.getAllContents().contains(rootObject)) { result.remove(i); --size; } else { ++i; } } result.add(object); } return result; } /** * Look for an ontology within the given document. * * @param in * An input stream for the document content * @param baseURI * A base URI for the resolution of relative URIs * @param mimeType * The MIME-type of the document * @return The ontology URI or <code>null</code> * * @throws Exception * If an error occurs while searching for the ontology */ public static String findOntology(InputStream in, String baseURI, String mimeType) throws Exception { if (mimeType == null && !in.markSupported()) { in = new BufferedInputStream(in); } final String[] ontology = { null }; RDFParser parser = Rio.createParser(determineFormat(mimeType, in)); parser.setRDFHandler(new RDFHandler() { @Override public void endRDF() throws RDFHandlerException { } @Override public void handleComment(String text) throws RDFHandlerException { } @Override public void handleNamespace(String prefx, String uri) throws RDFHandlerException { if (prefx.length() == 0) { // use empty prefix as fallback ontology[0] = URIs.createURI(uri).trimFragment().toString(); } } @Override public void handleStatement(Statement stmt) throws RDFHandlerException { if (RDF.TYPE.equals(stmt.getPredicate()) && OWL.ONTOLOGY.equals(stmt.getObject())) { if (stmt.getSubject() instanceof IRI) { ontology[0] = stmt.getSubject().stringValue(); throw new RDFHandlerException("found ontology URI"); } } } @Override public void startRDF() throws RDFHandlerException { } }); try { parser.parse(in, baseURI); } catch (RDFHandlerException rhe) { // ignore, ontology was found } finally { in.close(); } return ontology[0]; } /** * Compute a label for the given element. */ public static String getLabel(Object element) { return getLabel(element, false); } /** * Compute a label for the given element. * * @param element * The target element for which the label should be computed * @param useLabelForVocab * if <code>true</code> then the property rdfs:label is also used * for classes, properties and built-in vocabulary, else the URI * is used as label */ public static String getLabel(Object element, boolean useLabelForVocab) { if (element instanceof IStatement) { element = ((IStatement) element).getObject(); } if (element instanceof Resource) { Resource resource = (Resource) element; String label = null; if (useLabelForVocab || !(resource instanceof Class || resource instanceof Property || resource instanceof IObject && ((IObject) resource).isOntLanguageTerm())) { label = resource.getRdfsLabel(); } if (label != null) { return label; } else { return toPName(resource); } } else if (element instanceof ILiteral) { return ((ILiteral) element).getLabel(); } return element == null ? "" : element.toString(); } /** * Compute a prefixed name for the given element. */ public static String getPName(Object element) { if (element instanceof IStatement) { element = ((IStatement) element).getObject(); } if (element instanceof IEntity) { return toPName((IEntity) element); } return String.valueOf(element); } /** * Returns a map of supported MIME-types with associated priorities. * * The key of this map is the MIME-type and the value is its priority in the * range [0, 1]. * * @return Map of MIME-types with associated priorities */ public static Map<String, Double> getSupportedMimeTypes() { Map<String, Double> mimeTypes = new HashMap<>(); IContentTypeManager contentTypeManager = Platform .getContentTypeManager(); if (contentTypeManager != null) { for (IContentType contentType : contentTypeManager .getAllContentTypes()) { Object mimeType = mimeType(contentType.getDefaultDescription()); if (mimeType != null) { mimeTypes.put(mimeType.toString(), priorityForMimeType(mimeType.toString())); } } } else { for (RDFFormat format : RDFParserRegistry.getInstance().getKeys()) { for (String mimeType : format.getMIMETypes()) { mimeTypes.put(mimeType.toString(), priorityForMimeType(mimeType.toString())); } } } return mimeTypes; } /** * Returns the MIME-type for the given content description. * * @param contentDescription * A content description * * @return The associated MIME-type or <code>null</code> */ public static String mimeType(IContentDescription contentDescription) { if (contentDescription != null) { return (String) contentDescription.getProperty(new QualifiedName( ModelPlugin.PLUGIN_ID, "mimeType")); } return null; } /** * Returns the MIME-type for the file name or <code>null</code> if it is * unknown. * * @param fileName * A file name * * @return The associated MIME-type or <code>null</code> */ public static String mimeType(String fileName) { String mimeType = null; IContentTypeManager contentTypeManager = Platform .getContentTypeManager(); if (contentTypeManager != null) { IContentType contentType = contentTypeManager .findContentTypeFor(fileName); ; if (contentType != null) { mimeType = mimeType(contentType.getDefaultDescription()); } } if (mimeType == null) { Optional<RDFFormat> format = Rio.getParserFormatForFileName(fileName); if (format.isPresent()) { mimeType = format.get().getDefaultMIMEType(); } } return mimeType; } private static double priorityForMimeType(String mimeType) { if ("text/plain".equals(mimeType)) { return 0.5; } else if ("text/html".equals(mimeType)) { return 0.3; } return 1; } /** * Read data from an input stream and forward it to a {@link IDataVisitor * data visitor}. * * @param in * An input stream for the document content * @param baseURI * A base URI for the resolution of relative URIs * @param mimeType * The MIME-type of the document * @param preserveBNodeIDs * Control if blank node IDs should be preserved or new ones * should be generated * * @param dataVisitor * The data visitor which should consume the data */ public static <V extends IDataVisitor<?>> void readData(InputStream in, String baseURI, String mimeType, final boolean preserveBNodeIDs, final V dataVisitor) { if (mimeType == null && !in.markSupported()) { in = new BufferedInputStream(in); } ValueFactory valueFactory = new SimpleValueFactory() { @Override public synchronized BNode createBNode() { return super.createBNode(BlankNode.generateId("new-") .substring(2)); } @Override public synchronized BNode createBNode(String nodeID) { if (preserveBNodeIDs) { return super.createBNode(nodeID); } else { return super.createBNode("new-" + nodeID); } } }; final RDF4JValueConverter valueConverter = new RDF4JValueConverter( valueFactory); final boolean handleNamespaces = dataVisitor instanceof IDataAndNamespacesVisitor<?>; RDFParser parser = Rio.createParser(determineFormat(mimeType, in), valueFactory); parser.setPreserveBNodeIDs(preserveBNodeIDs); parser.setRDFHandler(new RDFHandler() { @Override public void endRDF() throws RDFHandlerException { dataVisitor.visitEnd(); } @Override public void handleComment(String text) throws RDFHandlerException { } @Override public void handleNamespace(String prefix, String uri) throws RDFHandlerException { if (handleNamespaces) { ((IDataAndNamespacesVisitor<?>) dataVisitor) .visitNamespace(new Namespace(prefix, URIs .createURI(uri))); } } @Override public void handleStatement(Statement stmt) throws RDFHandlerException { dataVisitor .visitStatement(new net.enilink.komma.core.Statement( (IReference) valueConverter.fromRdf4j(stmt .getSubject()), (IReference) valueConverter.fromRdf4j(stmt .getPredicate()), valueConverter .fromRdf4j(stmt.getObject()))); } @Override public void startRDF() throws RDFHandlerException { dataVisitor.visitBegin(); } }); try { parser.parse(in, baseURI); } catch (RDFParseException e) { throw new KommaException("Invalid RDF data:\n" + e.getMessage(), e); } catch (RDFHandlerException e) { throw new KommaException("Loading RDF failed", e); } catch (IOException e) { throw new KommaException("Cannot access RDF data", e); } finally { try { in.close(); } catch (IOException e) { throw new KommaException("Unable to close input stream", e); } } } /** * Read data from an input stream and forward it to a {@link IDataVisitor * data visitor}. * * This method generates new blank node IDs and does NOT preserve the ones * from the document. * * @param in * An input stream for the document content * @param baseURI * A base URI for the resolution of relative URIs * @param mimeType * The MIME-type of the document * * @param dataVisitor * The data visitor which should consume the data */ public static <V extends IDataVisitor<?>> void readData(InputStream in, String baseURI, String mimeType, final V dataVisitor) { // generate unique BNode IDs by default readData(in, baseURI, mimeType, false, dataVisitor); } private static String toPName(IEntity resource) { URI uri = resource.getURI(); if (uri != null) { String prefix = resource.getEntityManager().getPrefix( uri.namespace()); String localPart = uri.localPart(); boolean hasLocalPart = localPart != null && localPart.length() > 0; StringBuilder text = new StringBuilder(); if (prefix != null && prefix.length() > 0 && hasLocalPart) { text.append(prefix).append(":"); } if (hasLocalPart && prefix != null) { text.append(localPart); } else { text.append("<").append(uri).append(">"); } return text.toString(); } else { return resource.toString(); } } /** * Return a {@link IDataVisitor data visitor} that can be used to write data * to an output stream. * * @param out * An output stream for the document content * @param baseURI * A base URI for the resolution of relative URIs * @param mimeType * The MIME-type of the document * @param charset * The character set that should be used for writing the data or * <code>null</code> for the default character set of the * specified <code>mimeType</code> * * @return The data visitor for writing the data */ public static IDataAndNamespacesVisitor<Void> writeData(OutputStream os, String baseURI, String mimeType, String charset) { try { final RDFWriter writer = createWriter(os, baseURI, mimeType, charset); return new IDataAndNamespacesVisitor<Void>() { final RDF4JValueConverter valueConverter = new RDF4JValueConverter( SimpleValueFactory.getInstance()); void throwException(Exception e) { throw new KommaException("Saving RDF failed", e); } @Override public Void visitBegin() { try { writer.startRDF(); } catch (RDFHandlerException e) { throwException(e); } return null; } @Override public Void visitEnd() { try { writer.endRDF(); } catch (RDFHandlerException e) { throwException(e); } return null; } @Override public Void visitNamespace(INamespace namespace) { try { writer.handleNamespace(namespace.getPrefix(), namespace .getURI().toString()); } catch (RDFHandlerException e) { throwException(e); } return null; } @Override public Void visitStatement(IStatement stmt) { try { writer.handleStatement(valueConverter.toRdf4j(stmt)); } catch (RDFHandlerException e) { throwException(e); } return null; } }; } catch (IOException e) { throw new KommaException("Creating RDF writer failed", e); } } }