/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.jena.n3; import java.io.*; import java.math.BigDecimal; import java.net.URI; import java.net.URISyntaxException; import java.text.CharacterIterator; import java.text.StringCharacterIterator; import java.util.*; import java.util.Map.Entry; import org.apache.jena.JenaRuntime ; import org.apache.jena.rdf.model.* ; import org.apache.jena.rdf.model.impl.Util ; import org.apache.jena.shared.JenaException ; import org.apache.jena.util.iterator.ClosableIterator ; import org.apache.jena.util.iterator.WrappedIterator ; import org.apache.jena.vocabulary.OWL ; import org.apache.jena.vocabulary.RDF ; import org.apache.jena.vocabulary.XSD ; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** Common framework for implementing N3 writers. */ public class N3JenaWriterCommon implements RDFWriter { static Logger logger = LoggerFactory.getLogger(N3JenaWriterCommon.class) ; // N3 writing proceeds in 2 stages. // First, it analysis the model to be written to extract information // that is going to be specially formatted (RDF lists, one ref anon nodes) // Second do the writing walk. // The simple N3 writer does nothing during preparation. protected Map<String, Object> writerPropertyMap = null ; // BaseURI - <#> // final boolean doAbbreviatedBaseURIref = getBooleanValue("abbrevBaseURI", true) ; protected boolean alwaysAllocateBNodeLabel = false ; // Common variables protected RDFErrorHandler errorHandler = null; protected Map<String, String> prefixMap = new HashMap<>() ; // Prefixes to actually use protected Map<String, String> reversePrefixMap = new HashMap<>() ; // URI->prefix protected Map<Resource, String> bNodesMap = null ; // BNodes seen. protected int bNodeCounter = 0 ; // Specific properties that have a short form. // Not Turtle. protected static final String NS_W3_log = "http://www.w3.org/2000/10/swap/log#" ; protected static Map<String, String> wellKnownPropsMapN3 = new HashMap<>() ; static { wellKnownPropsMapN3.put(NS_W3_log+"implies", "=>" ) ; wellKnownPropsMapN3.put(OWL.sameAs.getURI(), "=" ) ; wellKnownPropsMapN3.put(RDF.type.getURI(), "a" ) ; } protected static Map<String, String> wellKnownPropsMapTurtle = new HashMap<>() ; static { //wellKnownPropsMapTurtle.put(OWL.sameAs.getURI(), "=" ) ; wellKnownPropsMapTurtle.put(RDF.type.getURI(), "a" ) ; } protected Map<String, String> wellKnownPropsMap = wellKnownPropsMapN3 ; // Work variables controlling the output protected N3IndentedWriter out = null ; //Removed base URI specials - look for "// BaseURI - <#>" & doAbbreviatedBaseURIref //String baseURIref = null ; //String baseURIrefHash = null ; // Min spacing of items protected int minGap = getIntValue("minGap", 1) ; protected String minGapStr = pad(minGap) ; // Gap from subject to property protected int indentProperty = getIntValue("indentProperty", 6) ; // Width of property before wrapping. // This is not necessarily a control of total width // e.g. the pretty writer may be writing properties inside indented one ref bNodes protected int widePropertyLen = getIntValue("widePropertyLen", 20) ; // Column for property when an object follows a property on the same line protected int propertyCol = getIntValue("propertyColumn", 8) ; // Minimum gap from property to object when object on a new line. protected int indentObject = propertyCol ; // If a subject is shorter than this, the first property may go on same line. protected int subjectColumn = getIntValue("subjectColumn", indentProperty) ; // Require shortSubject < subjectCol (strict less than) protected int shortSubject = subjectColumn-minGap; protected boolean useWellKnownPropertySymbols = getBooleanValue("usePropertySymbols", true) ; protected boolean allowTripleQuotedStrings = getBooleanValue("useTripleQuotedStrings", true) ; protected boolean allowDoubles = getBooleanValue("useDoubles", true) ; protected boolean allowDecimals = getBooleanValue("useDecimals", true) ; // ---------------------------------------------------- // Jena RDFWriter interface @Override public RDFErrorHandler setErrorHandler(RDFErrorHandler errHandler) { RDFErrorHandler old = errorHandler; errorHandler = errHandler; return old; } @Override public Object setProperty(String propName, Object propValue) { if ( ! ( propValue instanceof String ) ) { logger.warn("N3.setProperty: Property for '"+propName+"' is not a string") ; propValue = propValue.toString() ; } // Store absolute name of property propName = absolutePropName(propName) ; if ( writerPropertyMap == null ) writerPropertyMap = new HashMap<>() ; Object oldValue = writerPropertyMap.get(propName); writerPropertyMap.put(propName, propValue); return oldValue; } /** Write the model out in N3. The writer should be one suitable for UTF-8 which * excludes a PrintWriter or a FileWriter which use default character set. * * Examples: * <pre> * try { * Writer w = new BufferedWriter(new OutputStreamWriter(output, "UTF-8")) ; * model.write(w, base) ; * try { w.flush() ; } catch (IOException ioEx) {...} * } catch (java.io.UnsupportedEncodingException ex) {} //UTF-8 is required so can't happen * </pre> * or * <pre> * try { * OutputStream out = new FileOutputStream(file) ; * Writer w = new BufferedWriter(new OutputStreamWriter(out, "UTF-8")) ; * model.write(w, base) ; * } * catch (java.io.UnsupportedEncodingException ex) {} * catch (java.io.FileNotFoundException noFileEx) { ... } * </pre> * @see #write(Model,Writer,String) */ @Override public void write(Model baseModel, Writer _out, String base) { if (!(_out instanceof BufferedWriter)) _out = new BufferedWriter(_out); out = new N3IndentedWriter(_out); // BaseURI - <#> // if ( base != null ) // { // baseURIref = base ; // if ( !base.endsWith("#") &&! isOpaque(base) ) // baseURIrefHash = baseURIref+"#" ; // } processModel(baseModel) ; } /** Write the model out in N3, encoded in in UTF-8 * @see #write(Model,Writer,String) */ @Override public synchronized void write(Model model, OutputStream output, String base) { try { Writer w = new BufferedWriter(new OutputStreamWriter(output, "UTF-8")) ; write(model, w, base) ; try { w.flush() ; } catch (IOException ioEx) { throw new JenaException(ioEx) ; } } catch (java.io.UnsupportedEncodingException ex) { System.err.println("Failed to create UTF-8 writer") ; } } // ---------------------------------------------------- // The assumed processing model is: // Writing N3 involves ordering the graph into: // -- Subjects // -- Property lists within subjects // -- Object lists with in properties // A derived class may choose to intercept and implement at any of these levels. // Standard layout is: // subject // property1 value1 ; // property2 value2 ; // property3 value3 . // Normal hook points for subclasses. protected void startWriting() {} protected void finishWriting() {} protected void prepare(Model model) {} protected void processModel(Model model) { prefixMap = model.getNsPrefixMap() ; bNodesMap = new HashMap<>() ; for ( Iterator<Entry<String, String>> iter = prefixMap.entrySet().iterator() ; iter.hasNext() ; ) { Entry<String, String> e = iter.next() ; String prefix = e.getKey() ; String uri = e.getValue(); // XML namespaces name can include '.' // Turtle prefixed names can't. if ( ! checkPrefixPart(prefix) ) iter.remove() ; else { if ( checkPrefixPart(prefix) ) // Build acceptable reverse mapping reversePrefixMap.put(uri, prefix) ; } } startWriting() ; prepare(model) ; writeHeader(model) ; writePrefixes(model) ; if (prefixMap.size() != 0) out.println(); // Do the output. writeModel(model) ; // Release intermediate memory - allows reuse of a writer finishWriting() ; bNodesMap = null ; } protected void writeModel(Model model) { // Needed only for no prefixes, no blank first line. boolean doingFirst = true; ResIterator rIter = listSubjects(model); for (; rIter.hasNext();) { // Subject: // First - it is something we will write out as a structure in an object field? // That is, a RDF list or the object of exactly one statement. Resource subject = rIter.nextResource(); if ( skipThisSubject(subject) ) { if (N3JenaWriter.DEBUG) out.println("# Skipping: " + formatResource(subject)); continue; } // We really are going to print something via writeTriples if (doingFirst) doingFirst = false; else out.println(); writeOneGraphNode(subject) ; } rIter.close(); } protected ResIterator listSubjects(Model model) { return model.listSubjects(); } protected void writeOneGraphNode(Resource subject) { // New top level item. // Does not take effect until newline. out.incIndent(indentProperty) ; writeSubject(subject); ClosableIterator<Property> iter = preparePropertiesForSubject(subject); writePropertiesForSubject(subject, iter) ; out.decIndent(indentProperty) ; out.println(" ."); } protected void writePropertiesForSubject(Resource subj, ClosableIterator<Property> iter) { // For each property. for (; iter.hasNext();) { Property property = iter.next(); // Object list writeObjectList(subj, property); if (iter.hasNext()) out.println(" ;"); } iter.close(); } // Hook called on every resource. // Since there is spacing bewteen resource frames, need to know // whether an item will cause any output. protected boolean skipThisSubject(Resource r) { return false ; } // This is the hook called within writeModel. // NB May not be at the top level (indent = 0) protected void writeSubject(Resource subject) { String subjStr = formatResource(subject); out.print(subjStr); // May be very short : if so, stay on this line. // Currently at end of subject. // NB shortSubject is (subjectColumn-minGap) so there is a gap. if (subjStr.length() < shortSubject ) { out.print(pad(subjectColumn - subjStr.length()) ); } else // Does not fit this line. out.println(); } protected void writeHeader(Model model) { // BaseURI - <#> // if (baseURIref != null && !baseURIref.equals("") ) // out.println("# Base: " + baseURIref); } protected N3IndentedWriter getOutput() { return out ; } protected Map<String, String> getPrefixes() { return prefixMap ; } protected void writePrefixes(Model model) { for ( Entry<String, String> entry : prefixMap.entrySet() ) { String u = entry.getValue(); String p = entry.getKey(); // BaseURI - <#> // // Special cases: N3 handling of base names. // if (doAbbreviatedBaseURIref && p.equals("")) // { // if (baseURIrefHash != null && u.equals(baseURIrefHash)) // u = "#"; // if (baseURIref != null && u.equals(baseURIref)) // u = ""; // } String tmp = "@prefix " + p + ": "; out.print( tmp ); out.print( pad( 16 - tmp.length() ) ); // NB Starts with a space to ensure a gap. out.println( " <" + u + "> ." ); } } protected void writeObjectList(Resource subject, Property property) { String propStr = formatProperty(property) ; // if (wellKnownPropsMap.containsKey(property.getURI())) // propStr = (String) wellKnownPropsMap.get(property.getURI()); // else // propStr = formatResource(property); // Write with object lists as clusters of statements with the same property // Looks more like a machine did it but fewer bad cases. StmtIterator sIter = subject.listProperties(property); for (; sIter.hasNext();) { Statement stmt = sIter.nextStatement() ; String objStr = formatNode(stmt.getObject()) ; out.print(propStr); out.incIndent(indentObject); if ( (propStr.length()+minGap) <= widePropertyLen ) { // Property col allows for min gap but widePropertyLen > propertyCol // (which looses alignment - this is intentional. // Ensure there is at least min gap. int padding = calcPropertyPadding(propStr) ; out.print(pad(padding)) ; // if ( propStr.length() < propertyWidth ) // out.print( pad(propertyCol-minGap-propStr.length()) ) ; // out.print(minGapStr) ; } else // Does not fit this line. out.println(); // Write one object - simple writing. out.print(objStr) ; out.decIndent(indentObject); if ( sIter.hasNext() ) { out.println(" ;") ; } } sIter.close() ; } protected String formatNode(RDFNode node) { if (node instanceof Literal) return formatLiteral((Literal) node); else return formatResource((Resource)node) ; } protected void writeObject(RDFNode node) { if (node instanceof Literal) { writeLiteral((Literal) node); return; } Resource rObj = (Resource) node; out.print(formatResource(rObj)); } protected void writeLiteral(Literal literal) { out.print(formatLiteral(literal)) ; } protected ClosableIterator<Property> preparePropertiesForSubject(Resource r) { // Properties to do. Set<Property> properties = new HashSet<>() ; StmtIterator sIter = r.listProperties(); for ( ; sIter.hasNext() ; ) properties.add(sIter.nextStatement().getPredicate()) ; sIter.close() ; return WrappedIterator.create(properties.iterator()) ; } // Utility operations protected String formatResource(Resource r) { if ( r.isAnon() ) { if ( ! alwaysAllocateBNodeLabel ) { // Does anything point to it? StmtIterator sIter = r.getModel().listStatements(null, null, r) ; if ( ! sIter.hasNext() ) { sIter.close() ; // This bNode is not referenced so don't need the bNode Id. // Must be a subject - indent better be zero! // This only happens for subjects because object bNodes // referred to once (the other case for [] syntax) // are handled elsewhere (by oneRef set) // Later: use [ prop value ] for this. return "[]" ; } sIter.close() ; } if ( ! bNodesMap.containsKey(r) ) bNodesMap.put(r, "_:b"+(++bNodeCounter)) ; return bNodesMap.get(r) ; } // It has a URI. if ( r.equals(RDF.nil) ) return "()" ; return formatURI(r.getURI()) ; } protected String formatLiteral(Literal literal) { String datatype = literal.getDatatypeURI() ; String lang = literal.getLanguage() ; String s = literal.getLexicalForm() ; if ( datatype != null ) { // Special form we know how to handle? // Assume valid text if ( datatype.equals(XSD.integer.getURI()) ) { try { new java.math.BigInteger(s) ; return s ; } catch (NumberFormatException nfe) {} // No luck. Continue. // Continuing is always safe. } if ( datatype.equals(XSD.decimal.getURI()) ) { // Must have ., can't have e or E if ( s.indexOf('.') >= 0 && s.indexOf('e') == -1 && s.indexOf('E') == -1 ) { // Turtle - N3 does not allow .3 +.3 or -.3 // See if parsable. try { new BigDecimal(s); return s ; } catch (NumberFormatException nfe) {} } } if ( this.allowDoubles && datatype.equals(XSD.xdouble.getURI()) ) { // Must have 'e' or 'E' (N3 and Turtle now read 2.3 as a decimal). if ( s.indexOf('e') >= 0 || s.indexOf('E') >= 0 ) { try { // Validate it. Double.parseDouble(s) ; return s ; } catch (NumberFormatException nfe) {} // No luck. Continue. } } } // Format the text - with escaping. StringBuffer sbuff = new StringBuffer() ; boolean singleQuoteLiteral = true ; String quoteMarks = "\"" ; // Things that force the use of """ strings if ( this.allowTripleQuotedStrings && ( s.indexOf("\n") != -1 || s.indexOf("\r") != -1 || s.indexOf("\f") != -1 ) ) { quoteMarks = "\"\"\"" ; singleQuoteLiteral = false ; } sbuff.append(quoteMarks); string(sbuff, s, singleQuoteLiteral) ; sbuff.append(quoteMarks); if ( Util.isLangString(literal) ) { sbuff.append("@") ; sbuff.append(lang) ; } else if ( ! Util.isSimpleString(literal) ) { sbuff.append("^^") ; sbuff.append(formatURI(datatype)) ; } return sbuff.toString() ; } protected String formatProperty(Property p) { String prop = p.getURI() ; if ( this.useWellKnownPropertySymbols && wellKnownPropsMap.containsKey(prop) ) return wellKnownPropsMap.get(prop); return formatURI(prop) ; } protected String formatURI(String uriStr) { // BaseURI - <#> // if ( doAbbreviatedBaseURIref && uriStr.equals(baseURIref) ) // return "<>" ; // Try for a prefix and write as prefixed name. // 1/ Try splitting as a prefixed name // 2/ Search for possibilities // Stage 1. int idx = splitIdx(uriStr) ; // Depends on legal URIs. if ( idx >= 0 ) { // Include the # itself. String x = uriStr.substring(0,idx+1) ; String prefix = reversePrefixMap.get(x) ; if ( prefix != null ) { String localPart = uriStr.substring(idx+1) ; if ( checkNamePart(localPart) ) return prefix+':'+localPart ; } } // Unsplit. Could just return here. // // Find the longest if several. // // Possible optimization: split URI and have URI=> ns: map. // // Ordering prefixes by length, then first hit is better. // // // // Also: could just assume that the split is on / or # // // Means we need to find a prefix just once. // for ( Iterator<String> pIter = prefixMap.keySet().iterator() ; pIter.hasNext() ; ) // { // String p = pIter.next() ; // String u = prefixMap.get(p) ; // if ( uriStr.startsWith(u) ) // if ( matchURI.length() < u.length() ) // { // matchPrefix = p ; // matchURI = u ; // } // } // if ( matchPrefix != null ) // { // String localname = uriStr.substring(matchURI.length()) ; // // if ( checkPrefixedName(matchPrefix, localname) ) // return matchPrefix+":"+localname ; // // // Continue and return quoted URIref // } // Not as a prefixed name - write as a quoted URIref // It should be right - the writer should be UTF-8 on output. return "<"+uriStr+">" ; } protected static int splitIdx(String uriStr) { int idx = uriStr.lastIndexOf('#') ; if ( idx >= 0 ) return idx ; // No # - try for / idx = uriStr.lastIndexOf('/') ; return idx ; } // Checks of prefixed names // These tests must agree, or be more restrictive, than the parser. protected static boolean checkPrefixedName(String ns, String local) { return checkPrefixPart(ns) && checkNamePart(local) ; } /* http://www.w3.org/TeamSubmission/turtle/#sec-grammar-grammar * [27] qname ::= prefixName? ':' name? * [30] nameStartChar ::= [A-Z] | "_" | [a-z] | [#x00C0-#x00D6] | [#x00D8-#x00F6] | [#x00F8-#x02FF] | [#x0370-#x037D] | [#x037F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF] * [31] nameChar ::= nameStartChar | '-' | [0-9] | #x00B7 | [#x0300-#x036F] | [#x203F-#x2040] * [32] name ::= nameStartChar nameChar* * [33] prefixName ::= ( nameStartChar - '_' ) nameChar* */ protected static boolean checkPrefixPart(String s) { if ( s.length() == 0 ) return true; CharacterIterator cIter = new StringCharacterIterator(s) ; char ch = cIter.first() ; if ( ! checkNameStartChar(ch) ) return false ; if ( ch == '_' ) // Can't start with _ (bnodes labels handled separately) return false ; return checkNameTail(cIter) ; } protected static boolean checkNamePart(String s) { if ( s.length() == 0 ) return true; CharacterIterator cIter = new StringCharacterIterator(s) ; char ch = cIter.first() ; if ( ! checkNameStartChar(ch) ) return false ; return checkNameTail(cIter) ; } private static boolean checkNameTail(CharacterIterator cIter) { // Assumes cIter.first already called but nothing else. // Skip first char. char ch = cIter.next() ; for ( ; ch != java.text.CharacterIterator.DONE ; ch = cIter.next() ) { if ( ! checkNameChar(ch) ) return false ; } return true ; } protected static boolean checkNameStartChar(char ch) { if ( Character.isLetter(ch) ) return true ; if ( ch == '_' ) return true ; return false ; } protected static boolean checkNameChar(char ch) { if ( Character.isLetterOrDigit(ch) ) return true ; if ( ch == '_' ) return true ; if ( ch == '-' ) return true ; return false ; } protected final static String WS = "\n\r\t" ; protected static void string(StringBuffer sbuff, String s, boolean singleQuoteLiteral) { for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); // Escape escapes and quotes if (c == '\\' || c == '"' ) { sbuff.append('\\') ; sbuff.append(c) ; continue ; } // Characters to literally output. // This would generate 7-bit safe files // if (c >= 32 && c < 127) // { // sbuff.append(c) ; // continue; // } // Whitespace if ( singleQuoteLiteral && ( c == '\n' || c == '\r' || c == '\f' ) ) { if (c == '\n') sbuff.append("\\n"); if (c == '\t') sbuff.append("\\t"); if (c == '\r') sbuff.append("\\r"); if (c == '\f') sbuff.append("\\f"); continue ; } // Output as is (subject to UTF-8 encoding on output that is) sbuff.append(c) ; // // Unicode escapes // // c < 32, c >= 127, not whitespace or other specials // String hexstr = Integer.toHexString(c).toUpperCase(); // int pad = 4 - hexstr.length(); // sbuff.append("\\u"); // for (; pad > 0; pad--) // sbuff.append("0"); // sbuff.append(hexstr); } } protected int calcPropertyPadding(String propStr) { int padding = propertyCol - propStr.length(); if (padding < minGap) padding = minGap; return padding ; } protected static String pad(int cols) { StringBuffer sb = new StringBuffer() ; for ( int i = 0 ; i < cols ; i++ ) sb.append(' ') ; return sb.toString() ; } // Utilities protected int countProperties(Resource r) { int numProp = 0 ; StmtIterator sIter = r.listProperties() ; for ( ; sIter.hasNext() ; ) { sIter.nextStatement() ; numProp++ ; } sIter.close() ; return numProp ; } protected int countProperties(Resource r, Property p) { int numProp = 0 ; StmtIterator sIter = r.listProperties(p) ; for ( ; sIter.hasNext() ; ) { sIter.nextStatement() ; numProp++ ; } sIter.close() ; return numProp ; } protected int countArcsTo(Resource resource) { return countArcsTo(null, resource) ; } protected int countArcsTo(Property prop, Resource resource) { int numArcs = 0 ; StmtIterator sIter = resource.getModel().listStatements(null, prop, resource) ; for ( ; sIter.hasNext() ; ) { sIter.nextStatement() ; numArcs++ ; } sIter.close() ; return numArcs ; } protected Iterator<RDFNode> rdfListIterator(Resource r) { List<RDFNode> list = new ArrayList<>() ; for ( ; ! r.equals(RDF.nil); ) { StmtIterator sIter = r.getModel().listStatements(r, RDF.first, (RDFNode)null) ; list.add(sIter.nextStatement().getObject()) ; if ( sIter.hasNext() ) // @@ need to cope with this (unusual) case throw new JenaException("N3: Multi valued list item") ; sIter = r.getModel().listStatements(r, RDF.rest, (RDFNode)null) ; r = (Resource)sIter.nextStatement().getObject() ; if ( sIter.hasNext() ) throw new JenaException("N3: List has two tails") ; } return list.iterator() ; } // Convenience operations for accessing system properties. protected String getStringValue(String prop, String defaultValue) { String p = getPropValue(prop) ; if ( p == null ) return defaultValue ; return p ; } protected boolean getBooleanValue(String prop, boolean defaultValue) { String p = getPropValue(prop) ; if ( p == null ) return defaultValue ; if ( p.equalsIgnoreCase("true") ) return true ; if ( p.equals("1") ) return true ; return false ; } protected int getIntValue(String prop, int defaultValue) { String p = getPropValue(prop) ; if ( p == null ) return defaultValue ; try { return Integer.parseInt(p) ; } catch (NumberFormatException ex) { logger.warn("Format error for property: "+prop) ; return defaultValue ; } } // May be the absolute or local form of the property name protected String getPropValue(String prop) { prop = absolutePropName(prop) ; if ( writerPropertyMap != null && writerPropertyMap.containsKey(prop) ) { Object obj = writerPropertyMap.get(prop) ; if ( ! ( obj instanceof String ) ) logger.warn("getPropValue: N3 Property for '"+prop+"' is not a string") ; return (String)obj ; } String s = JenaRuntime.getSystemProperty(prop) ; if ( s == null ) s = JenaRuntime.getSystemProperty(localPropName(prop)) ; return s ; } protected String absolutePropName(String propName) { if ( propName.indexOf(':') == -1 ) return N3JenaWriter.propBase + propName ; return propName ; } protected String localPropName(String propName) { if ( propName.startsWith(N3JenaWriter.propBase) ) propName = propName.substring(N3JenaWriter.propBase.length()) ; return propName ; } private boolean isOpaque(String uri) { try { return new URI(uri).isOpaque() ; } catch (URISyntaxException ex) { return true ; } } }