This is an automated email from the ASF dual-hosted git repository.

andy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git

commit 452816cf97d8dba621d1c25b4b0032a24c0b9c7b
Author: Andy Seaborne <[email protected]>
AuthorDate: Sun Aug 11 12:31:32 2024 +0100

    GH-2630: Handle unqualified attributes
---
 .../apache/jena/riot/lang/rdfxml/package-info.java |   9 +-
 .../jena/riot/lang/rdfxml/rrx/ParserRRX_SAX.java   | 274 ++++++++++++---------
 .../lang/rdfxml/rrx_stax_ev/ParserRRX_StAX_EV.java | 245 +++++++++++-------
 .../lang/rdfxml/rrx_stax_sr/ParserRRX_StAX_SR.java | 235 +++++++++++-------
 .../jena/riot/lang/rdfxml/rrx/RunTestRDFXML.java   |   6 +-
 .../apache/jena/riot/lang/rdfxml/rrx/TestRRX.java  |  22 +-
 .../testing/RIOT/rrx-files/bad-rdf-id-node.rdf     |   2 +-
 .../RIOT/rrx-files/bad-rdf-resource-node.rdf       |   2 +-
 .../RIOT/rrx-files/bad-unqualified-attribute1.rdf  |   2 +-
 .../RIOT/rrx-files/bad-unqualified-attribute2.rdf  |   2 +-
 ...tribute2.rdf => bad-unqualified-attribute3.rdf} |   6 +-
 ...tribute2.rdf => bad-unqualified-attribute4.rdf} |   6 +-
 ...tribute2.rdf => bad-unqualified-attribute5.rdf} |   6 +-
 13 files changed, 495 insertions(+), 322 deletions(-)

diff --git 
a/jena-arq/src/main/java/org/apache/jena/riot/lang/rdfxml/package-info.java 
b/jena-arq/src/main/java/org/apache/jena/riot/lang/rdfxml/package-info.java
index 1fad47ab40..348641f1be 100644
--- a/jena-arq/src/main/java/org/apache/jena/riot/lang/rdfxml/package-info.java
+++ b/jena-arq/src/main/java/org/apache/jena/riot/lang/rdfxml/package-info.java
@@ -22,16 +22,19 @@
  * that is the RIOT reader interface implementation and a {@code 
RDFXMLParser-???}
  * class which is the parser engine.
  * <ul>
- * <li>SAX based {@linkplain 
org.apache.jena.riot.lang.rdfxml.rrx.ParserRDFXML_SAX ParserRDFXML_SAX}. This 
is the default parser for Jena5 onwards for
+ * <li>SAX based {@linkplain 
org.apache.jena.riot.lang.rdfxml.rrx.ParserRRX_SAX ParserRRX_SAX}. This is the 
default parser for Jena5 onwards for
  * RDF/XML in RIOT, i.e it is the registration for {@linkplain 
org.apache.jena.riot.Lang#RDFXML Lang.RDFXML} and it is the fastest.
  * <li>A StAX-based parser using {@link javax.xml.stream.XMLStreamReader 
XMLStreamReader}.
  * <li>A StAX-based parser using {@link javax.xml.stream.XMLEventReader 
XMLEventReader}.
  * </ul>
- * In addition Jena (in jena-core) has the original ARP parser in package 
{@code org.apach.jena-rdfxml.rdfxml0} referred to as "ARP0".
- * This was the RDF/XML parser up to Jena 4.6.1. ARP1 uses package jena-iri 
directly to handle IRIs.
+ * In addition, Jena (in jena-core) has the original ARP parser.
+ * <p>
+ * "ARP0" in package {@code org.apach.jena-rdfxml.rdfxml0}
+ * This was the RDF/XML parser up to Jena 4.6.1. It handled IRIs directly 
using jena-iri.
  * <p>
  * "ARP1" is in package {@code org.apach.jena-rdfxml.rdfxml1} is derived from 
ARP0.
  * It uses the {@link org.apache.jena.irix.IRIx IRIx} abstraction to handle 
IRIs.
+ * It has error reporting aligned with RIOT.
  * <br/>
  * </p>
  * <table border=1 style="border-width: 2px ; border-style: solid; 
border-collapse: collapse">
diff --git 
a/jena-arq/src/main/java/org/apache/jena/riot/lang/rdfxml/rrx/ParserRRX_SAX.java
 
b/jena-arq/src/main/java/org/apache/jena/riot/lang/rdfxml/rrx/ParserRRX_SAX.java
index 9f34f9cd2e..6d3e768ef5 100644
--- 
a/jena-arq/src/main/java/org/apache/jena/riot/lang/rdfxml/rrx/ParserRRX_SAX.java
+++ 
b/jena-arq/src/main/java/org/apache/jena/riot/lang/rdfxml/rrx/ParserRRX_SAX.java
@@ -108,6 +108,7 @@ class ParserRRX_SAX
     private static Set<String> $coreSyntaxTerms =
             Set.of(rdfRDF, rdfID, rdfAbout, rdfParseType, rdfResource, 
rdfNodeID, rdfDatatype);
 
+    // Not used.
     // 6.2.3 Production syntaxTerms
     // coreSyntaxTerms | rdf:Description | rdf:li
     private static Set<String> $syntaxTerms =
@@ -118,9 +119,16 @@ class ParserRRX_SAX
     // rdf:aboutEach | rdf:aboutEachPrefix | rdf:bagID
     private static Set<String> $oldTerms = Set.of(rdfAboutEach, 
rdfAboutEachPrefix, rdfBagID);
 
-    private static Set<String> disallowedPropertyAttributeTerms =
-            Set.of(rdfRDF, rdfID, rdfAbout, rdfParseType, rdfResource, 
rdfNodeID, rdfDatatype,
-                   rdfDescription, rdfContainerItem, "aboutEach", 
"aboutEachPrefix", "bagID");
+    // 6.1.4 Attribute Event
+    // The (old form) qualified named allowed where property attributes 
expected.
+    // Parses "MAY" warn about their use
+    private static Set<String> $allowedUnqualified = Set.of(rdfAbout, rdfID, 
rdfResource, rdfParseType, rdfType);
+
+    private boolean coreSyntaxTerm(String namespace, String localName) {
+        if ( ! rdfNS.equals(namespace) )
+            return false;
+        return $coreSyntaxTerms.contains(localName);
+    }
 
     // 6.2.5 Production nodeElementURIs
     // anyURI - ( coreSyntaxTerms | rdf:li | oldTerms )
@@ -166,11 +174,20 @@ class ParserRRX_SAX
         return true;
     }
 
+    private static boolean allowedUnqualifiedTerm(String localName) {
+        return $allowedUnqualified.contains(localName);
+    }
+
+    /** The attributes that guide the RDF/XML parser. */
+    //  Production: CodeSyntaxterms
+    //      rdf:RDF | rdf:ID | rdf:about | rdf:parseType | rdf:resource | 
rdf:nodeID | rdf:datatype
+
     /** The attributes that guide the RDF/XML parser. */
     private static Set<String> $rdfSyntaxAttributes =
             Set.of(rdfRDF, rdfAbout, rdfNodeID, rdfID, rdfParseType, 
rdfDatatype, rdfResource);
-    private boolean isSyntaxAttribute(String namespace, String localName) {
-        if ( ! rdfNS.equals(namespace) )
+
+    private static boolean isSyntaxAttribute(String namespace, String 
localName) {
+        if ( ! isXMLNamespace(namespace) )
             return false;
         return $rdfSyntaxAttributes.contains(localName);
     }
@@ -250,6 +267,13 @@ class ParserRRX_SAX
         ObjectNode
     }
 
+    /** Mark the usage of a QName */
+    private enum QNameUsage {
+        TypedNodeElement("typed node element"), PropertyElement("property 
element");
+        final String msg;
+        private QNameUsage(String msg) { this.msg = msg; }
+    }
+
     /** Integer holder for rdf:li */
     private static class Counter { int value = 1; }
 
@@ -424,8 +448,6 @@ class ParserRRX_SAX
         this.parserMode = parserMode;
     }
 
-// // Forming objects.
-// private ParseType parseType = null;
     ParserRRX_SAX(String xmlBase, ParserProfile parserProfile, StreamRDF 
destination, Context context) {
         // Debug
         if ( ReaderRDFXML_SAX.TRACE ) {
@@ -696,12 +718,12 @@ class ParserRRX_SAX
                 if ( isNotRecognizedRDFtype(namespaceURI, localName) )
                     RDFXMLparseWarning(qName+" is not a recognized RDF term 
for a type", position);
             }
-            Node object = qNameToIRI(namespaceURI, localName, position, "typed 
node element");
+            Node object = qNameToIRI(namespaceURI, localName, 
QNameUsage.TypedNodeElement, position);
             emit(currentSubject, RDF.Nodes.type, object, position);
         }
 
-        if ( hasPropertyAttributes(attributes, position) )
-            processPropertyAttributes(currentSubject, attributes, position);
+        processPropertyAttributes(currentSubject, qName, attributes, false, 
position);
+
         parserMode(ParserMode.PropertyElement);
     }
 
@@ -726,7 +748,7 @@ class ParserRRX_SAX
             currentProperty = iri(p, position);
         } else {
             // The empty string namespace does not apply to XML attributes.
-            currentProperty = qNameToIRI(namespaceURI, localName, position, 
"property element");
+            currentProperty = qNameToIRI(namespaceURI, localName, 
QNameUsage.PropertyElement, position);
         }
 
         if ( ReaderRDFXML_SAX.TRACE )
@@ -763,14 +785,9 @@ class ParserRRX_SAX
         if ( objBlankNodeLabel != null )
             resourceObj = blankNode(objBlankNodeLabel, position);
 
-        if ( hasPropertyAttributes(attributes, position) ) {
-            if ( parseTypeStr != null  ) {
-                // rdf:parseType found.
-                throw RDFXMLparseError("The attribute rdf:parseType is not 
permitted with property attributes on a property element: "+qName, position);
-            }
+        Node innerSubject = processPropertyAttributes(resourceObj, qName, 
attributes, true, position);
+        if (resourceObj == null && innerSubject != null ) {
             // AND must be empty tag
-            Node innerSubject = (resourceObj==null) ? blankNode(position) : 
resourceObj;
-            processPropertyAttributes(innerSubject, attributes, position);
             currentEmitter.emit(currentSubject, currentProperty, innerSubject, 
position);
             return;
         }
@@ -828,34 +845,27 @@ class ParserRRX_SAX
         return currentProperty == null;
     }
 
-//    private String xmlBaseStr(Attributes attributes, Position position) {
-//        String baseStr = attributes.getValue(xmlNS, xmlBaseLN);
-//        if ( baseStr == null )
-//            return null;
-//        return IRIs.resolve(currentBase, baseStr);
-//    }
-
-        // Start element encountered when expecting a ObjectCollection
-        private void startCollectionItem(String namespaceURI, String 
localName, String qName, Attributes attributes, Position position) {
-            // Finish last list cell, start new one.
-            if ( ReaderRDFXML_SAX.TRACE )
-                trace.println("Generate list cell");
-            // Preceding cell in list.
-            Node previousCollectionNode = collectionNode.node;
-            Node thisCollectionNode = blankNode(position);
-            // New cell in list.
-            // Either link up to the origin or fixup previous cell.
-            if ( previousCollectionNode == null )
-                currentEmitter.emit(currentSubject, currentProperty, 
thisCollectionNode, position);
-            else
-                emit(previousCollectionNode, Nodes.rest, thisCollectionNode, 
position);
-            collectionNode.node = thisCollectionNode;
+    // Start element encountered when expecting a ObjectCollection
+    private void startCollectionItem(String namespaceURI, String localName, 
String qName, Attributes attributes, Position position) {
+        // Finish last list cell, start new one.
+        if ( ReaderRDFXML_SAX.TRACE )
+            trace.println("Generate list cell");
+        // Preceding cell in list.
+        Node previousCollectionNode = collectionNode.node;
+        Node thisCollectionNode = blankNode(position);
+        // New cell in list.
+        // Either link up to the origin or fixup previous cell.
+        if ( previousCollectionNode == null )
+            currentEmitter.emit(currentSubject, currentProperty, 
thisCollectionNode, position);
+        else
+            emit(previousCollectionNode, Nodes.rest, thisCollectionNode, 
position);
+        collectionNode.node = thisCollectionNode;
 
-            // Start the item.
-            Node itemSubject = attributesToSubjectNode(attributes, position);
-            emit(thisCollectionNode, RDF.Nodes.first, itemSubject, position);
-            startNodeElementWithSubject(itemSubject, namespaceURI, localName, 
qName, attributes, position);
-        }
+        // Start the item.
+        Node itemSubject = attributesToSubjectNode(attributes, position);
+        emit(thisCollectionNode, RDF.Nodes.first, itemSubject, position);
+        startNodeElementWithSubject(itemSubject, namespaceURI, localName, 
qName, attributes, position);
+    }
 
     private void endCollectionItem(Position position) {
         if ( ReaderRDFXML_SAX.TRACE )
@@ -952,86 +962,122 @@ class ParserRRX_SAX
             currentLang = xmlLang;
     }
 
-    // Property attributes.
-    // The checking is done by the call to hasPropertyAttributes.
-    private void processPropertyAttributes(Node subject, Attributes 
attributes, Position position) {
-        for ( int i = 0 ; i < attributes.getLength() ; i++ ) {
-            boolean isPropertyAttribute = checkPropertyAttribute(attributes, 
i, false, position);
-            if ( ! isPropertyAttribute )
-                continue;
-            propertyAttribute(subject, attributes, i, position);
+    // Process property attributes - return null for nothing to do.
+    private Node processPropertyAttributes(Node resourceObj, String qName, 
Attributes attributes, boolean isPropertyElement, Position position) {
+        // Subject may not yet be decided.
+        List<Integer> indexes = gatherPropertyAttributes(attributes, position);
+        if ( indexes.isEmpty() )
+            return null;
+        if ( isPropertyElement ) {
+            String parseTypeStr = attributes.getValue(rdfNS, rdfParseType);
+            if ( parseTypeStr != null ) {
+                  // rdf:parseType found.
+                  throw RDFXMLparseError("The attribute rdf:parseType is not 
permitted with property attributes on a property element: "+qName, position);
+            }
         }
+
+        Node innerSubject = (resourceObj==null) ? blankNode(position) : 
resourceObj;
+        outputPropertyAttributes(innerSubject, indexes, attributes, position);
+        return innerSubject;
     }
 
-    // Early abort! But also used to avoid creating a Node for
-    // processPropertyAttributes which has no work to do.
-    private boolean hasPropertyAttributes(Attributes attributes, Position 
position) {
-        for ( int i = 0 ; i < attributes.getLength() ; i++ ) {
-            boolean isPropertyAttribute = checkPropertyAttribute(attributes, 
i, true, position);
-            if ( ! isPropertyAttribute )
-                continue;
-            return true;
+    private List<Integer> gatherPropertyAttributes(Attributes attributes, 
Position position) {
+        if ( attributes.getLength() == 0 )
+            return List.of();
+        // Indexes
+        List<Integer> attributeIdx = new ArrayList<>(attributes.getLength());
+        for ( int idx = 0 ; idx < attributes.getLength() ; idx++ ) {
+            boolean isPropertyAttribute = checkPropertyAttribute(attributes, 
idx, position);
+            if ( isPropertyAttribute )
+                attributeIdx.add(idx);
+        }
+        return attributeIdx;
+    }
+
+    private void outputPropertyAttributes(Node subject, List<Integer> indexes, 
Attributes attributes, Position position) {
+        for ( int index : indexes ) {
+            String namespaceURI = attributes.getURI(index);
+            String localName = attributes.getLocalName(index);
+            String value = attributes.getValue(index);
+
+            if ( rdfNS.equals(namespaceURI) ) {
+                if ( rdfType.equals(localName) ) {
+                    Node type = iriResolve(value, position);
+                    emit(subject, Nodes.type, type, position);
+                    return;
+                }
+            }
+            Node property = attributeToIRI(namespaceURI, localName, position);
+            String lex = value;
+            Node object = literal(lex, currentLang, position);
+            emit(subject, property, object, position);
         }
-        return false;
     }
 
-    /** Return true if this is a property attribute. */
-    private boolean checkPropertyAttribute(Attributes attributes, int index, 
boolean outputWarnings, Position position) {
+    // 6.1.2 Element Event - attributes
+    /** Return true if this is an acceptable property attribute. */
+    private boolean checkPropertyAttribute(Attributes attributes, int index, 
Position position) {
         String namespaceURI = attributes.getURI(index);
         String localName = attributes.getLocalName(index);
         String qName = attributes.getQName(index);
 
-        if ( StringUtils.isBlank(namespaceURI) ) {
-            // Note about XML: The empty string namespace does not apply to 
XML attributes,
-            // only XML elements. ":attr" is not legal XML.
-            //RDFXMLparseError("XML attribute '"+qName+"' used for RDF 
property attribute (no namespace)", position);
-
-            if ( outputWarnings ){
-                if ( ! localName.isEmpty() )    // Skip XML namespace 
declarations.
-                    RDFXMLparseWarning("XML attribute '"+qName+"' used for RDF 
property attribute - ignored", position);
-            }
-            return false;
-        }
         if ( isSyntaxAttribute(namespaceURI, localName) )
             return false;
 
+        // The default namespace (i.e. prefix "") does not apply to attributes.
+
+        // 6.1.2 Element Event - attributes
+
+        if ( coreSyntaxTerm(namespaceURI, localName) )
+            return false;
+
         if ( ! allowedPropertyAttributeURIs(namespaceURI, localName) )
-            throw RDFXMLparseError("Not allowed as a property attribute: 
'"+attributes.getQName(index)+"'", position);
+            throw RDFXMLparseError("Not allowed as a property attribute 
'"+attributes.getQName(index)+"'", position);
 
-        if ( outputWarnings && isNotRecognizedRDFproperty(namespaceURI, 
localName) )
+        if ( isNotRecognizedRDFproperty(namespaceURI, localName) )
             RDFXMLparseWarning(qName+" is not a recognized RDF term for a 
property attribute", position);
 
+        // xml:lang, xml:base, xml:space (if these get here).
         if ( isXMLQName(namespaceURI, localName) )
             return false;
 
+        // 6.1.2 Element Event
+        // "All remaining reserved XML Names (see Name in XML 1.0) are now 
removed from the set."
         if ( isXMLNamespace(namespaceURI) ) {
             // Unrecognized qnames in the XMLnamespace are a warning and are 
ignored.
-            RDFXMLparseWarning("Unrecognized XML attribute: 
'"+attributes.getQName(index)+"'", position);
+            RDFXMLparseWarning("Unrecognized XML attribute '"+qName+"' - 
ignored", position);
             return false;
         }
 
-        if ( isXMLNamespaceQName(qName) )
+        if ( isXMLNamespaceQName(qName) ) {
+            // xmlns
             return false;
+        }
+
+        if ( StringUtils.isBlank(namespaceURI) ) {
+            boolean valid = checkPropertyAttributeUnqualifiedTerm(localName, 
position);
+            return valid;
+        }
         return true;
     }
 
-    /** Output for a property attribute (already checked) */
-    private void propertyAttribute(Node subject, Attributes attributes, int 
index, Position position) {
-        String namespaceURI = attributes.getURI(index);
-        String localName = attributes.getLocalName(index);
-        String value = attributes.getValue(index);
-
-        if ( rdfNS.equals(namespaceURI) ) {
-            if ( rdfType.equals(localName) ) {
-                Node type = iriResolve(value, position);
-                emit(subject, Nodes.type, type, position);
-                return;
+    private boolean checkPropertyAttributeUnqualifiedTerm(String localName, 
Position position) {
+        if ( allowedUnqualifiedTerm(localName) )
+            return true;
+        if ( localName.length() >= 3 ) {
+            String chars3 = localName.substring(0, 3);
+            if ( chars3.equalsIgnoreCase("xml") ) {
+                // 6.1.2 Element Event
+                // "all attribute information items with [prefix] property 
having no value and which
+                // have [local name] beginning with xml (case independent 
comparison)"
+                // Test: unrecognised-xml-attributes/test002.rdf
+                RDFXMLparseWarning("Unrecognized XML non-namespaced attribute 
'"+localName+"' - ignored", position);
+                return false;
             }
         }
-        Node property = qNameToIRI(namespaceURI, localName, position, 
"property attribute");
-        String lex = value;
-        Node object = literal(lex, currentLang, position);
-        emit(subject, property, object, position);
+        // 6.1.4 Attribute Event
+        // "Other non-namespaced ·local-name· accessor values are forbidden."
+        throw RDFXMLparseError("Non-namespaced attribute not allowed as a 
property attribute: '"+localName+"'", position);
     }
 
     /**
@@ -1095,13 +1141,6 @@ class ParserRRX_SAX
         return (s, p, o, loc) -> emitReify(reify, s, p, o, loc);
     }
 
-//    private String xmlBaseStr(Attributes attributes, Position position) {
-//        String baseStr = attributes.getValue(xmlNS, xmlBaseLN);
-//        if ( baseStr == null )
-//            return null;
-//        return IRIs.resolve(currentBase, baseStr);
-//    }
-
     private Node generateLiteral(Position position) {
         String lex = accCharacters.toString();
         if ( datatype != null )
@@ -1271,19 +1310,33 @@ class ParserRRX_SAX
 
     // ---- Creating terms.
 
-    private Node qNameToIRI(String namespaceURI, String localName, Position 
position, String usage) {
+    private Node qNameToIRI(String namespaceURI, String localName, QNameUsage 
usage, Position position) {
         if ( StringUtils.isBlank(namespaceURI) )
-            throw RDFXMLparseError("Unqualified "+usage+" not allowed: 
<"+localName+">", position);
-        String uriStr = qNameToIRI(namespaceURI, localName);
+            // Default namespace, "" not defined.
+            throw RDFXMLparseError("Unqualified "+usage.msg+" not allowed: 
<"+localName+">", position);
+        String uriStr = strQNameToIRI(namespaceURI, localName);
         return iri(uriStr, position);
     }
 
     /** This is the RDF rule for creating an IRI from a QName. */
-    private String qNameToIRI(String namespaceURI, String localName) {
+    private String strQNameToIRI(String namespaceURI, String localName) {
         String iriStr = namespaceURI + localName;
         return iriStr;
     }
 
+    private Node attributeToIRI(String namespaceURI, String localName, 
Position position) {
+        String ns = namespaceURI;
+        if ( StringUtils.isBlank(namespaceURI) ) {
+            if ( allowedUnqualifiedTerm(localName) )
+                ns = rdfNS;
+            else
+                // else rejected in checkPropertyAttribute
+                throw RDFXMLparseError("Unqualified property attribute not 
allowed: '"+localName+"'", position);
+        }
+        String uriStr = strQNameToIRI(namespaceURI, localName);
+        return iri(uriStr, position);
+    }
+
     private Node iri(String uriStr, Position position) {
         Objects.requireNonNull(uriStr);
         Objects.requireNonNull(position);
@@ -1342,17 +1395,6 @@ class ParserRRX_SAX
         return parserProfile.createURI(iriStr, line, col);
     }
 
-//    private Node iriDirect(String uriStr, Position position) {
-//        Objects.requireNonNull(uriStr);
-//        // No checking.
-//        return createURIdirect(uriStr, position);
-//    }
-//
-//    /** Always done without checking. */
-//    private Node createURIdirect(String uriStr, Position position) {
-//        return factory.createURI(uriStr);
-//    }
-
     private Node blankNode(Position position) {
         Objects.requireNonNull(position);
         int line = position.line();
diff --git 
a/jena-arq/src/main/java/org/apache/jena/riot/lang/rdfxml/rrx_stax_ev/ParserRRX_StAX_EV.java
 
b/jena-arq/src/main/java/org/apache/jena/riot/lang/rdfxml/rrx_stax_ev/ParserRRX_StAX_EV.java
index 84e97ead58..f0706cdcd4 100644
--- 
a/jena-arq/src/main/java/org/apache/jena/riot/lang/rdfxml/rrx_stax_ev/ParserRRX_StAX_EV.java
+++ 
b/jena-arq/src/main/java/org/apache/jena/riot/lang/rdfxml/rrx_stax_ev/ParserRRX_StAX_EV.java
@@ -91,6 +91,13 @@ class ParserRRX_StAX_EV {
         currentLang = frame.lang;
     }
 
+    /** Mark the usage of a QName */
+    private enum QNameUsage {
+        TypedNodeElement("typed node element"), PropertyElement("property 
element");
+        final String msg;
+        private QNameUsage(String msg) { this.msg = msg; }
+    }
+
     // ---- Error handlers
 
     private RiotException RDFXMLparseError(String message, XMLEvent event) {
@@ -212,19 +219,25 @@ class ParserRRX_StAX_EV {
     private static Set<QName> $coreSyntaxTerms =
             Set.of(rdfRDF, rdfID, rdfAbout, rdfParseType, rdfResource, 
rdfNodeID, rdfDatatype);
 
-    // 6.2.3 Production syntaxTerms
-    // coreSyntaxTerms | rdf:Description | rdf:li
-    private static Set<QName> $syntaxTerms =
-            Set.of(rdfRDF, rdfID, rdfAbout, rdfParseType, rdfResource, 
rdfNodeID, rdfDatatype,
-                   rdfDescription, rdfContainerItem);
+//    // 6.2.3 Production syntaxTerms
+//    // coreSyntaxTerms | rdf:Description | rdf:li
+//    private static Set<QName> $syntaxTerms =
+//            Set.of(rdfRDF, rdfID, rdfAbout, rdfParseType, rdfResource, 
rdfNodeID, rdfDatatype,
+//                   rdfDescription, rdfContainerItem);
 
     // 6.2.4 Production oldTerms
     // rdf:aboutEach | rdf:aboutEachPrefix | rdf:bagID
     private static Set<QName> $oldTerms = Set.of(rdfAboutEach, 
rdfAboutEachPrefix, rdfBagID);
 
-    private static Set<QName> disallowedPropertyAttributeTerms =
-            Set.of(rdfRDF, rdfID, rdfAbout, rdfParseType, rdfResource, 
rdfNodeID, rdfDatatype,
-                   rdfDescription, rdfContainerItem, rdfAboutEach, 
rdfAboutEachPrefix, rdfBagID);
+    private static Set<String> $allowedUnqualified =
+            Set.of(rdfAbout.getLocalPart(), rdfID.getLocalPart(), 
rdfResource.getLocalPart(),
+                   rdfParseType.getLocalPart(), rdfType.getLocalPart());
+
+    private boolean coreSyntaxTerm(QName qName) {
+        if ( ! rdfNS.equals(qName.getNamespaceURI()) )
+            return false;
+        return $coreSyntaxTerms.contains(qName);
+    }
 
     // 6.2.5 Production nodeElementURIs
     // anyURI - ( coreSyntaxTerms | rdf:li | oldTerms )
@@ -270,6 +283,10 @@ class ParserRRX_StAX_EV {
         return true;
     }
 
+    private static boolean allowedUnqualifiedTerm(String localName) {
+        return $allowedUnqualified.contains(localName);
+    }
+
     /** The attributes that guide the RDF/XML parser. */
     private static Set<QName> $rdfSyntaxAttributes =
             Set.of(rdfRDF, rdfAbout, rdfNodeID, rdfID, rdfParseType, 
rdfDatatype, rdfResource);
@@ -374,8 +391,6 @@ class ParserRRX_StAX_EV {
         // ---- Node Loop - sequence of zero or more RDF/XML node elements.
         // generic move over multiple elements
         while ( event != null ) {
-//            if ( VERBOSE )
-//                out.println("-- node loop: "+str(event));
             if ( !event.isStartElement() )
                 break;
             StartElement startElt = event.asStartElement();
@@ -387,8 +402,6 @@ class ParserRRX_StAX_EV {
 
     /** Top level single node element. (no &lt;rdf:RDF&gt;, &lt;/rdf:RDF&gt;) 
*/
     private XMLEvent nodeElementSingle(XMLEvent event) {
-//          if ( VERBOSE )
-//              out.println("-- single node element: "+str(event));
         if ( !event.isStartElement() )
             // Protective "not recognized"
             return event;
@@ -448,15 +461,14 @@ class ParserRRX_StAX_EV {
                     RDFXMLparseWarning(str(qName)+" is not a recognized RDF 
term for a type", location);
             }
 
-            Node object = qNameToIRI(qName, location, "typed node element");
+            Node object = qNameToIRI(qName, QNameUsage.TypedNodeElement, 
location);
             emit(subject, NodeConst.nodeRDFType, object, location);
         }
 
         // Other attributes are properties.
         // rdf:type is special.
 
-        if ( hasAttributeProperties(startElt) )
-            processPropertyAttributes(subject, startElt, location);
+        processPropertyAttributes(subject, startElt, true, location);
 
         // Finished with the node start tag.
         XMLEvent event = nextEventTag();
@@ -471,79 +483,11 @@ class ParserRRX_StAX_EV {
         return event.asEndElement();
     }
 
-    // Property attributes.
-    // The checking is done by the call to hasPropertyAttributes.
-    private void processPropertyAttributes(Node subject, StartElement 
startElt, Location location) {
-        // This both nodes and properties. There is an implicit bnode in when 
used with properties.
-        // attributes don't support location :: attribute.getLocation();
-        Iterator<Attribute> x = startElt.getAttributes();
-        while (x.hasNext()) {
-            Attribute attribute = x.next();
-            boolean isPropertyAttribute = 
checkPropertyAttribute(attribute.getName(), startElt, false);
-            if ( ! isPropertyAttribute )
-                continue;
-            QName qName = attribute.getName();
-            if ( rdfType.equals(qName) ) {
-                String iriStr = attribute.getValue();
-                Node type = iriResolve(iriStr, location);
-                emit(subject, RDF.Nodes.type, type, location);
-                continue;
-            }
-            Node property = qNameToIRI(attribute.getName(), location, 
"property attribute");
-            Node object = literal(attribute.getValue(), currentLang, location);
-            emit(subject, property, object, location);
-        }
-    }
-
-    private boolean hasAttributeProperties(StartElement startElt) {
-        Iterator<Attribute> x = startElt.getAttributes();
-        while (x.hasNext()) {
-            Attribute attribute = x.next();
-            boolean isPropertyAttribute = 
checkPropertyAttribute(attribute.getName(), startElt, true);
-            if ( ! isPropertyAttribute )
-                continue;
-            return true;
-        }
-        return false;
-    }
-
-    /** Return true if this is a property attribute. */
-    private boolean checkPropertyAttribute(QName qName, XMLEvent event, 
boolean outputWarnings) {
-        String namespace = qName.getNamespaceURI();
-        String localName = qName.getLocalPart();
-        if ( namespace == null || namespace.isEmpty() ) {
-            //RDFXMLparseError("XML attribute '"+localName+"' used for RDF 
property attribute (no namespace)", event);
-            if ( outputWarnings )
-                RDFXMLparseWarning("XML attribute '"+localName+"' used for RDF 
property attribute - ignored", event);
-            return false;
-        }
-        if ( isSyntaxAttribute(qName) )
-            return false;
-
-        if ( ! allowedPropertyAttributeURIs(qName) )
-            throw RDFXMLparseError("Not allowed as a property attribute: 
'"+str(qName)+"'", event);
-
-        if ( outputWarnings && isNotRecognizedRDFproperty(qName) )
-            RDFXMLparseWarning(str(qName)+" is not a recognized RDF term for a 
property attribute", event);
-
-        if ( isXMLQName(qName) )
-            return false;
-
-        if ( isXMLNamespace(qName) ) {
-            // Unrecognized qnames in the XMLnamespace are a warning and are 
ignored.
-            RDFXMLparseWarning("Unrecognized XML attribute: '"+str(qName)+"'", 
event);
-            return false;
-        }
-        return true;
-    }
-
     // ---- Property elements
 
     private XMLEvent propertyElementLoop(Node subject, XMLEvent event) {
         Counter listElementCounter = new Counter();
         while (event != null) {
-//            if ( VERBOSE )
-//                out.println("-- property loop: "+str(event));
             if ( ! event.isStartElement() )
                 break;
             propertyElement(subject, event.asStartElement(), 
listElementCounter);
@@ -587,7 +531,7 @@ class ParserRRX_StAX_EV {
         if ( qNameMatches(rdfContainerItem, startElt.getName()) )
             property = 
iriDirect(rdfNS+"_"+Integer.toString(listElementCounter.value++), location);
         else
-            property = qNameToIRI(startElt.getName(), location, "property 
element");
+            property = qNameToIRI(startElt.getName(), 
QNameUsage.PropertyElement, location);
 
         Node reify = reifyStatement(startElt);
         Emitter emitter = (reify==null) ? this::emit : 
(s,p,o,loc)->emitReify(reify, s, p, o, loc);
@@ -617,13 +561,8 @@ class ParserRRX_StAX_EV {
         if ( objBlanklNodeLabel != null )
             resourceObj = blankNode(objBlanklNodeLabel, location);
 
-        if ( hasAttributeProperties(startElt) ) {
-            if ( parseType != parseTypePlain  ) {
-                // rdf:parseType found.
-                throw RDFXMLparseError("The attribute rdf:parseType is not 
permitted with property attributes on a property element: "+startElt.getName(), 
startElt);
-            }
-            Node innerSubject = (resourceObj==null) ? blankNode(location) : 
resourceObj;
-            processPropertyAttributes(innerSubject, startElt, location);
+        Node innerSubject = processPropertyAttributes(resourceObj, startElt, 
true, location);
+        if ( resourceObj == null && innerSubject != null ) {
             emitter.emit(subject, property, innerSubject, location);
             XMLEvent event = nextEventAny();
             if ( !event.isEndElement() )
@@ -731,6 +670,108 @@ class ParserRRX_StAX_EV {
         return event;
     }
 
+    private Node processPropertyAttributes(Node resourceObj, StartElement 
startElt, boolean isPropertyElement, Location location) {
+        // Subject may not yet be decided.
+        List<Attribute> propertyAttributes = 
gatherPropertyAttributes(startElt, location);
+        if ( propertyAttributes.isEmpty() )
+            return null;
+        if ( isPropertyElement ) {
+            String parseTypeStr = objectParseType(startElt);
+            if ( parseTypeStr != parseTypePlain ) {
+                  // rdf:parseType found.
+                  throw RDFXMLparseError("The attribute rdf:parseType is not 
permitted with property attributes on a property element: 
"+str(startElt.getName()), location);
+            }
+        }
+
+        Node innerSubject = (resourceObj==null) ? blankNode(location) : 
resourceObj;
+        outputPropertyAttributes(innerSubject, propertyAttributes, location);
+        return innerSubject;
+    }
+
+    private List<Attribute> gatherPropertyAttributes(StartElement startElt, 
Location location) {
+        Iterator<Attribute> x = startElt.getAttributes();
+        if ( ! x.hasNext() )
+            return List.of();
+        List<Attribute> propertyAttributes = new ArrayList<>();
+        while(x.hasNext()) {
+            Attribute attribute = x.next();
+            boolean isPropertyAttribute = 
checkPropertyAttribute(attribute.getName(), location);
+            if ( isPropertyAttribute )
+                propertyAttributes.add(attribute);
+        }
+        return propertyAttributes;
+    }
+
+    private void outputPropertyAttributes(Node subject, List<Attribute> 
propertyAttributes, Location location) {
+        for ( Attribute attribute : propertyAttributes ) {
+            QName qName =  attribute.getName();
+            if ( rdfType.equals(qName) ) {
+                String iriStr = attribute.getValue();
+                Node type = iriResolve(iriStr, location);
+                emit(subject, RDF.Nodes.type, type, location);
+                return;
+            }
+            Node property = attributeToIRI(qName, location);
+            String lexicalForm = attribute.getValue();
+            Node object = literal(lexicalForm, currentLang, location);
+            emit(subject, property, object, location);
+        }
+    }
+
+    /** Return true if this is a property attribute. */
+    private boolean checkPropertyAttribute(QName qName, Location location) {
+        if ( isSyntaxAttribute(qName) )
+            return false;
+
+        // The default namespace (i.e. prefix "") does not apply to attributes.
+
+        // 6.1.2 Element Event - attributes
+
+        if (coreSyntaxTerm(qName) )
+            return false;
+
+        if ( ! allowedPropertyAttributeURIs(qName) )
+            throw RDFXMLparseError("Not allowed as a property attribute: 
'"+str(qName)+"'", location);
+
+        if ( isNotRecognizedRDFproperty(qName) )
+            RDFXMLparseWarning(str(qName)+" is not a recognized RDF term for a 
property attribute", location);
+
+        if ( isXMLQName(qName) )
+            return false;
+
+        if ( isXMLNamespace(qName) ) {
+            // Unrecognized qnames in the XMLnamespace are a warning and are 
ignored.
+            RDFXMLparseWarning("Unrecognized XML attribute: '"+str(qName)+"'", 
location);
+            return false;
+        }
+
+        if ( StringUtils.isBlank(qName.getNamespaceURI()) ) {
+            String localName = qName.getLocalPart();
+            boolean valid = checkPropertyAttributeUnqualifiedTerm(localName, 
location);
+            return valid;
+        }
+        return true;
+    }
+
+    private boolean checkPropertyAttributeUnqualifiedTerm(String localName, 
Location location) {
+        if ( allowedUnqualifiedTerm(localName) )
+            return true;
+        if ( localName.length() >= 3 ) {
+            String chars3 = localName.substring(0, 3);
+            if ( chars3.equalsIgnoreCase("xml") ) {
+                // 6.1.2 Element Event
+                // "all attribute information items with [prefix] property 
having no value and which
+                // have [local name] beginning with xml (case independent 
comparison)"
+                // Test: unrecognised-xml-attributes/test002.rdf
+                RDFXMLparseWarning("Unrecognized XML non-namespaced attribute 
'"+localName+"' - ignored", location);
+                return false;
+            }
+        }
+        // 6.1.4 Attribute Event
+        // "Other non-namespaced ·local-name· accessor values are forbidden."
+        throw RDFXMLparseError("Non-namespaced attribute not allowed as a 
property attribute: '"+localName+"'", location);
+    }
+
     private String objectParseType(StartElement startElt) {
         String parseTypeStr = attribute(startElt, rdfParseType);
         return  ( parseTypeStr != null ) ? parseTypeStr : parseTypePlain;
@@ -1126,18 +1167,32 @@ class ParserRRX_StAX_EV {
         parserProfile.setBaseIRI(n.getURI());
     }
 
-    private Node qNameToIRI(QName qName, Location location, String usage) {
+    private Node qNameToIRI(QName qName, QNameUsage usage, Location location) {
         if ( StringUtils.isBlank(qName.getNamespaceURI()) )
-            throw RDFXMLparseError("Unqualified "+usage+" not allowed: 
<"+qName.getLocalPart()+">", location);
-        String uriStr = qNameToIRI(qName);
+            throw RDFXMLparseError("Unqualified "+usage.msg+" not allowed: 
<"+qName.getLocalPart()+">", location);
+        String uriStr = strQNameToIRI(qName);
         return iriDirect(uriStr, location);
     }
 
     /** This is the RDF rule for creating an IRI from a QName. */
-    private String qNameToIRI(QName qName) {
+    private String strQNameToIRI(QName qName) {
         return qName.getNamespaceURI()+qName.getLocalPart();
     }
 
+    private Node attributeToIRI(QName qName, Location location) {
+        String namespaceURI = qName.getNamespaceURI();
+        String localName = qName.getLocalPart();
+        if ( StringUtils.isBlank(namespaceURI) ) {
+            if ( allowedUnqualifiedTerm(localName) )
+                namespaceURI = rdfNS;
+            else
+                // else rejected in checkPropertyAttribute
+                throw RDFXMLparseError("Unqualified property attribute not 
allowed: '"+localName+"'", location);
+        }
+        String uriStr = namespaceURI+localName;
+        return iriDirect(uriStr, location);
+    }
+
     // ---- Reading XMLEvents
 
     /** XML parsing error. */
diff --git 
a/jena-arq/src/main/java/org/apache/jena/riot/lang/rdfxml/rrx_stax_sr/ParserRRX_StAX_SR.java
 
b/jena-arq/src/main/java/org/apache/jena/riot/lang/rdfxml/rrx_stax_sr/ParserRRX_StAX_SR.java
index a8eaa8402e..d6df70f8af 100644
--- 
a/jena-arq/src/main/java/org/apache/jena/riot/lang/rdfxml/rrx_stax_sr/ParserRRX_StAX_SR.java
+++ 
b/jena-arq/src/main/java/org/apache/jena/riot/lang/rdfxml/rrx_stax_sr/ParserRRX_StAX_SR.java
@@ -94,6 +94,13 @@ class ParserRRX_StAX_SR {
         currentLang = frame.lang;
     }
 
+    /** Mark the usage of a QName */
+    private enum QNameUsage {
+        TypedNodeElement("typed node element"), PropertyElement("property 
element");
+        final String msg;
+        private QNameUsage(String msg) { this.msg = msg; }
+    }
+
     // ---- Error handlers
 
     private RiotException RDFXMLparseError(String message) {
@@ -222,9 +229,16 @@ class ParserRRX_StAX_SR {
     // rdf:aboutEach | rdf:aboutEachPrefix | rdf:bagID
     private static Set<QName> $oldTerms = Set.of(rdfAboutEach, 
rdfAboutEachPrefix, rdfBagID);
 
-    private static Set<QName> disallowedPropertyAttributeTerms =
-            Set.of(rdfRDF, rdfID, rdfAbout, rdfParseType, rdfResource, 
rdfNodeID, rdfDatatype,
-                   rdfDescription, rdfContainerItem, rdfAboutEach, 
rdfAboutEachPrefix, rdfBagID);
+    private static Set<String> $allowedUnqualified =
+            Set.of(rdfAbout.getLocalPart(), rdfID.getLocalPart(), 
rdfResource.getLocalPart(),
+                   rdfParseType.getLocalPart(), rdfType.getLocalPart());
+
+    private boolean coreSyntaxTerm(QName qName) {
+        if ( ! rdfNS.equals(qName.getNamespaceURI()) )
+            return false;
+        return $coreSyntaxTerms.contains(qName);
+    }
+
 
     // 6.2.5 Production nodeElementURIs
     // anyURI - ( coreSyntaxTerms | rdf:li | oldTerms )
@@ -270,6 +284,10 @@ class ParserRRX_StAX_SR {
         return true;
     }
 
+    private static boolean allowedUnqualifiedTerm(String localName) {
+        return $allowedUnqualified.contains(localName);
+    }
+
     /** The attributes that guide the RDF/XML parser. */
     private static Set<QName> $rdfSyntaxAttributes =
             Set.of(rdfRDF, rdfAbout, rdfNodeID, rdfID, rdfParseType, 
rdfDatatype, rdfResource);
@@ -368,8 +386,6 @@ class ParserRRX_StAX_SR {
 
     /** Top level single node element. (no &lt;rdf:RDF&gt;, &lt;/rdf:RDF&gt;) 
*/
     private void nodeElementSingle(int eventType) {
-//          if ( VERBOSE )
-//              out.println("-- single node element: "+str(event));
         if ( ! lookingAt(eventType, START_ELEMENT) )
             return ;
         nodeElement();
@@ -428,15 +444,11 @@ class ParserRRX_StAX_SR {
                     RDFXMLparseWarning(str(qName)+" is not a recognized RDF 
term for a type");
             }
 
-            Node object = qNameToIRI(qName, location, "type node element");
+            Node object = qNameToIRI(qName, QNameUsage.TypedNodeElement, 
location);
             emit(subject, NodeConst.nodeRDFType, object, location);
         }
 
-        // Other attributes are properties.
-        // rdf:type is special.
-
-        if ( hasAttributeProperties() )
-            processPropertyAttributes(subject, location);
+        processPropertyAttributes(subject, qName, false, location);
 
         // Finished with the node start tag.
         int event = nextEventTag();
@@ -446,80 +458,11 @@ class ParserRRX_StAX_SR {
             throw RDFXMLparseError("Expected end element for "+qName());
     }
 
-    // Property attributes.
-    // The checking is done by the call to hasPropertyAttributes.
-    private void processPropertyAttributes(Node subject, Location location) {
-        // This both nodes and properties. There is an implicit bnode in when 
used with properties.
-        int N = xmlSource.getAttributeCount();
-        for ( int i = 0 ; i < N ; i++ ) {
-            QName qName =  xmlSource.getAttributeName(i);
-            boolean isPropertyAttribute = checkPropertyAttribute(qName, false);
-            if ( ! isPropertyAttribute )
-                continue;
-            if ( rdfType.equals(qName) ) {
-                String iriStr = xmlSource.getAttributeValue(i);
-                Node type = iriResolve(iriStr, location);
-                emit(subject, RDF.Nodes.type, type, location);
-                continue;
-            }
-            Node property = qNameToIRI(qName, location, "property attribute");
-            String lexicalForm =  xmlSource.getAttributeValue(i);
-            Node object = literal(lexicalForm, currentLang, location);
-            emit(subject, property, object, location);
-        }
-    }
-
-    private boolean hasAttributeProperties() {
-        int N = xmlSource.getAttributeCount();
-        for ( int i = 0 ; i < N ; i++ ) {
-            QName qName =  xmlSource.getAttributeName(i);
-            boolean isPropertyAttribute = checkPropertyAttribute(qName, true);
-            if ( ! isPropertyAttribute )
-                continue;
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * Return true if this is a property attribute.
-     * @param qName
-     */
-    private boolean checkPropertyAttribute(QName qName, boolean 
outputWarnings) {
-        if ( StringUtils.isBlank(qName.getNamespaceURI()) ) {
-            //RDFXMLparseError("XML attribute '"+localName+"' used for RDF 
property attribute (no namespace)", event);
-            if ( outputWarnings )
-                RDFXMLparseWarning("XML attribute '"+qName.getLocalPart()+"' 
used for RDF property attribute - ignored");
-            return false;
-        }
-
-        if ( isSyntaxAttribute(qName) )
-            return false;
-
-        if ( ! allowedPropertyAttributeURIs(qName) )
-            throw RDFXMLparseError("Not allowed as a property attribute: 
'"+str(qName)+"'");
-
-        if ( outputWarnings && isNotRecognizedRDFproperty(qName) )
-            RDFXMLparseWarning(str(qName)+" is not a recognized RDF term for a 
property attribute");
-
-        if ( isXMLQName(qName) )
-            return false;
-
-        if ( isXMLNamespace(qName) ) {
-            // Unrecognized qnames in the XMLnamespace are a warning and are 
ignored.
-            RDFXMLparseWarning("Unrecognized XML attribute: '"+str(qName)+"'");
-            return false;
-        }
-        return true;
-    }
-
     // ---- Property elements
 
     private int propertyElementlLoop(Node subject, int event) {
         Counter listElementCounter = new Counter();
         while (true) {
-//            if ( VERBOSE )
-//                out.println("-- property loop: "+str(event));
             if ( ! lookingAt(event, START_ELEMENT) )
                 break;
             propertyElement(subject, listElementCounter, location());
@@ -562,7 +505,7 @@ class ParserRRX_StAX_SR {
         if ( qNameMatches(rdfContainerItem, qName) )
             property = 
iriDirect(rdfNS+"_"+Integer.toString(listElementCounter.value++), location());
         else
-            property = qNameToIRI(qName, location, "property element");
+            property = qNameToIRI(qName, QNameUsage.PropertyElement, location);
 
         Node reify = reifyStatement(location);
         Emitter emitter = (reify==null) ? this::emit : 
(s,p,o,loc)->emitReify(reify, s, p, o, loc);
@@ -592,13 +535,8 @@ class ParserRRX_StAX_SR {
         if ( objBlanklNodeLabel != null )
             resourceObj = blankNode(objBlanklNodeLabel, location);
 
-        if ( hasAttributeProperties() ) {
-            if ( parseType != parseTypePlain  ) {
-                // rdf:parseType found.
-                throw RDFXMLparseError("The attribute rdf:parseType is not 
permitted with property attributes on a property element: "+str(qName));
-            }
-            Node innerSubject = (resourceObj==null) ? blankNode(location) : 
resourceObj;
-            processPropertyAttributes(innerSubject, location);
+        Node innerSubject = processPropertyAttributes(resourceObj, qName, 
true, location);
+        if ( resourceObj == null && innerSubject != null ) {
             emitter.emit(subject, property, innerSubject, location);
             int event = nextEventAny();
             if ( ! lookingAt(event, END_ELEMENT) )
@@ -704,6 +642,109 @@ class ParserRRX_StAX_SR {
         return event;
     }
 
+    private Node processPropertyAttributes(Node resourceObj, QName qName, 
boolean isPropertyElement, Location location) {
+        // Subject may not yet be decided.
+        List<Integer> indexes = gatherPropertyAttributes(location);
+        if ( indexes.isEmpty() )
+            return null;
+        if ( isPropertyElement ) {
+            String parseTypeStr = objectParseType();
+            if ( parseTypeStr != parseTypePlain ) {
+                  // rdf:parseType found.
+                  throw RDFXMLparseError("The attribute rdf:parseType is not 
permitted with property attributes on a property element: "+str(qName), 
location);
+            }
+        }
+
+        Node innerSubject = (resourceObj==null) ? blankNode(location) : 
resourceObj;
+        outputPropertyAttributes(innerSubject, indexes, location);
+        return innerSubject;
+    }
+
+    private List<Integer> gatherPropertyAttributes(Location location) {
+        int N = xmlSource.getAttributeCount();
+        if ( N == 0 )
+            return List.of();
+        List<Integer> attributeIdx = new ArrayList<>(N);
+        for ( int idx = 0 ; idx < N ; idx++ ) {
+            QName qName =  xmlSource.getAttributeName(idx);
+            boolean isPropertyAttribute = checkPropertyAttribute(qName, 
location);
+            if ( isPropertyAttribute )
+                attributeIdx.add(idx);
+        }
+        return attributeIdx;
+    }
+
+    private void outputPropertyAttributes(Node subject, List<Integer> indexes, 
Location location) {
+        for ( int index : indexes ) {
+            QName qName =  xmlSource.getAttributeName(index);
+            if ( rdfType.equals(qName) ) {
+                String iriStr = xmlSource.getAttributeValue(index);
+                Node type = iriResolve(iriStr, location);
+                emit(subject, RDF.Nodes.type, type, location);
+                return;
+            }
+            Node property = attributeToIRI(qName, location);
+            String lexicalForm =  xmlSource.getAttributeValue(index);
+            Node object = literal(lexicalForm, currentLang, location);
+            emit(subject, property, object, location);
+        }
+    }
+
+    /** Return true if this is a property attribute. */
+    private boolean checkPropertyAttribute(QName qName, Location location) {
+        if ( isSyntaxAttribute(qName) )
+            return false;
+
+        // The default namespace (i.e. prefix "") does not apply to attributes.
+
+        // 6.1.2 Element Event - attributes
+
+        if (coreSyntaxTerm(qName) )
+            return false;
+
+        if ( ! allowedPropertyAttributeURIs(qName) )
+            throw RDFXMLparseError("Not allowed as a property attribute: 
'"+str(qName)+"'");
+
+        if ( isNotRecognizedRDFproperty(qName) )
+            RDFXMLparseWarning(str(qName)+" is not a recognized RDF term for a 
property attribute");
+
+        if ( isXMLQName(qName) )
+            return false;
+
+        if ( isXMLNamespace(qName) ) {
+            // Unrecognized qnames in the XMLnamespace are a warning and are 
ignored.
+            RDFXMLparseWarning("Unrecognized XML attribute: '"+str(qName)+"'");
+            return false;
+        }
+
+        if ( StringUtils.isBlank(qName.getNamespaceURI()) ) {
+            String localName = qName.getLocalPart();
+            boolean valid = checkPropertyAttributeUnqualifiedTerm(localName, 
location);
+            return valid;
+        }
+        return true;
+    }
+
+    private boolean checkPropertyAttributeUnqualifiedTerm(String localName, 
Location location) {
+        if ( allowedUnqualifiedTerm(localName) )
+            return true;
+        if ( localName.length() >= 3 ) {
+            String chars3 = localName.substring(0, 3);
+            if ( chars3.equalsIgnoreCase("xml") ) {
+                // 6.1.2 Element Event
+                // "all attribute information items with [prefix] property 
having no value and which
+                // have [local name] beginning with xml (case independent 
comparison)"
+                // Test: unrecognised-xml-attributes/test002.rdf
+                RDFXMLparseWarning("Unrecognized XML non-namespaced attribute 
'"+localName+"' - ignored", location);
+                return false;
+            }
+        }
+        // 6.1.4 Attribute Event
+        // "Other non-namespaced ·local-name· accessor values are forbidden."
+        throw RDFXMLparseError("Non-namespaced attribute not allowed as a 
property attribute: '"+localName+"'", location);
+    }
+
+
     private String objectParseType() {
         String parseTypeStr = attribute(rdfParseType);
         return  ( parseTypeStr != null ) ? parseTypeStr : parseTypePlain;
@@ -1082,9 +1123,9 @@ class ParserRRX_StAX_SR {
     }
 
     /** This is the RDF rule for creating an IRI from a QName. */
-    private Node qNameToIRI(QName qName, Location location, String usage) {
+    private Node qNameToIRI(QName qName, QNameUsage usage, Location location) {
         if ( StringUtils.isBlank(qName.getNamespaceURI()) )
-            throw RDFXMLparseError("Unqualified "+usage+" not allowed: 
<"+qName.getLocalPart()+">", location);
+            throw RDFXMLparseError("Unqualified "+usage.msg+" not allowed: 
<"+qName.getLocalPart()+">", location);
         String uriStr = strQNameToIRI(qName);
         return iriDirect(uriStr, location);
     }
@@ -1094,6 +1135,20 @@ class ParserRRX_StAX_SR {
         return qName.getNamespaceURI()+qName.getLocalPart();
     }
 
+    private Node attributeToIRI(QName qName, Location location) {
+        String namespaceURI = qName.getNamespaceURI();
+        String localName = qName.getLocalPart();
+        if ( StringUtils.isBlank(namespaceURI) ) {
+            if ( allowedUnqualifiedTerm(localName) )
+                namespaceURI = rdfNS;
+            else
+                // else rejected in checkPropertyAttribute
+                throw RDFXMLparseError("Unqualified property attribute not 
allowed: '"+localName+"'", location);
+        }
+        String uriStr = namespaceURI+localName;
+        return iriDirect(uriStr, location);
+    }
+
     // ---- Reading XML
 
     /** Get the string value for an attribute by QName */
diff --git 
a/jena-arq/src/test/java/org/apache/jena/riot/lang/rdfxml/rrx/RunTestRDFXML.java
 
b/jena-arq/src/test/java/org/apache/jena/riot/lang/rdfxml/rrx/RunTestRDFXML.java
index 2f12d64e41..a66196e947 100644
--- 
a/jena-arq/src/test/java/org/apache/jena/riot/lang/rdfxml/rrx/RunTestRDFXML.java
+++ 
b/jena-arq/src/test/java/org/apache/jena/riot/lang/rdfxml/rrx/RunTestRDFXML.java
@@ -372,8 +372,8 @@ public class RunTestRDFXML {
                 checkErrorHandler(testLabel, expectedErrorHandler, 
errorHandlerTest);
             return;
         } catch(RiotException ex) {
-            output.println("## "+testLabel);
-            ex.printStackTrace();
+//            output.println("## "+testLabel);
+//            ex.printStackTrace();
             fail("Unexpected parse error: "+ex.getMessage());
         }
     }
@@ -407,7 +407,7 @@ public class RunTestRDFXML {
         ErrorHandlerCollector actualErrorHandler = new ErrorHandlerCollector();
         assertThrows(RiotException.class, ()->{
             parseFile(testSubjectFactory, actualErrorHandler, filename);
-            output.printf("## Expected RiotExpection : %-4s : %s : %s", 
subjectLabel, testLabel, filename);
+            output.printf("## Expected RiotExpection : %-4s : %s : %s\n", 
subjectLabel, testLabel, filename);
         });
 
         if ( expectedErrorHandler != null )
diff --git 
a/jena-arq/src/test/java/org/apache/jena/riot/lang/rdfxml/rrx/TestRRX.java 
b/jena-arq/src/test/java/org/apache/jena/riot/lang/rdfxml/rrx/TestRRX.java
index 44235fb102..887ae1dfea 100644
--- a/jena-arq/src/test/java/org/apache/jena/riot/lang/rdfxml/rrx/TestRRX.java
+++ b/jena-arq/src/test/java/org/apache/jena/riot/lang/rdfxml/rrx/TestRRX.java
@@ -72,8 +72,6 @@ public class TestRRX {
         this.lang = lang;
     }
 
-    // XXX Track files!
-
     private static Set<String> processedFiles = new HashSet<>();
     private void trackFilename(String filename) {
         processedFiles.add(filename);
@@ -171,6 +169,26 @@ public class TestRRX {
         checkForError("bad-unqualified-property.rdf", false);
     }
 
+    @Test public void bad_unqualified_attribute1() {
+        checkForError("bad-unqualified-attribute1.rdf", false);
+    }
+
+    @Test public void bad_unqualified_attribute2() {
+        checkForError("bad-unqualified-attribute2.rdf", false);
+    }
+
+    @Test public void bad_unqualified_attribute3() {
+        checkForError("bad-unqualified-attribute3.rdf", false);
+    }
+
+    @Test public void bad_unqualified_attribute4() {
+        warningTest("bad-unqualified-attribute4.rdf", 1);
+    }
+
+    @Test public void bad_unqualified_attribute5() {
+        warningTest("bad-unqualified-attribute5.rdf", 1);
+    }
+
     @Test public void bad_unqualified_class() {
         checkForError("bad-unqualified-class.rdf", false);
     }
diff --git a/jena-arq/testing/RIOT/rrx-files/bad-rdf-id-node.rdf 
b/jena-arq/testing/RIOT/rrx-files/bad-rdf-id-node.rdf
index db19f039ff..071ac9b2ed 100644
--- a/jena-arq/testing/RIOT/rrx-files/bad-rdf-id-node.rdf
+++ b/jena-arq/testing/RIOT/rrx-files/bad-rdf-id-node.rdf
@@ -11,4 +11,4 @@
   <rdf:Description rdf:about="http://example/s"; rdf:ID="foo">
     <ex:property>text</ex:property>
   </rdf:Description>
-</rdf:RDF>
\ No newline at end of file
+</rdf:RDF>
diff --git a/jena-arq/testing/RIOT/rrx-files/bad-rdf-resource-node.rdf 
b/jena-arq/testing/RIOT/rrx-files/bad-rdf-resource-node.rdf
index ee3bbab131..b29db442a2 100644
--- a/jena-arq/testing/RIOT/rrx-files/bad-rdf-resource-node.rdf
+++ b/jena-arq/testing/RIOT/rrx-files/bad-rdf-resource-node.rdf
@@ -11,4 +11,4 @@
   <rdf:Description rdf:about="http://example/s"; 
rdf:resource="http://example/foo";>
     <ex:property>text</ex:property>
   </rdf:Description>
-</rdf:RDF>
\ No newline at end of file
+</rdf:RDF>
diff --git a/jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute1.rdf 
b/jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute1.rdf
index dd1db8e7c3..5957021ab9 100644
--- a/jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute1.rdf
+++ b/jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute1.rdf
@@ -3,4 +3,4 @@
 
 <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";>
   <rdf:Description name="NAME"/>
-</rdf:RDF>
\ No newline at end of file
+</rdf:RDF>
diff --git a/jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute2.rdf 
b/jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute2.rdf
index 21940b5d63..9a0bf21edf 100644
--- a/jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute2.rdf
+++ b/jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute2.rdf
@@ -6,4 +6,4 @@
     xmlns="http://example/";>
   <!-- qnames do not apply to attributes unless ex:attr -->
   <rdf:Description name="NAME"/>
-</rdf:RDF>
\ No newline at end of file
+</rdf:RDF>
diff --git a/jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute2.rdf 
b/jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute3.rdf
similarity index 66%
copy from jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute2.rdf
copy to jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute3.rdf
index 21940b5d63..be9dbd3fce 100644
--- a/jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute2.rdf
+++ b/jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute3.rdf
@@ -4,6 +4,6 @@
 <rdf:RDF 
     xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
     xmlns="http://example/";>
-  <!-- qnames do not apply to attributes unless ex:attr -->
-  <rdf:Description name="NAME"/>
-</rdf:RDF>
\ No newline at end of file
+  <!-- less that 3 characters attribute -->
+  <rdf:Description n="NAME"/>
+</rdf:RDF>
diff --git a/jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute2.rdf 
b/jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute4.rdf
similarity index 66%
copy from jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute2.rdf
copy to jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute4.rdf
index 21940b5d63..400c1db00e 100644
--- a/jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute2.rdf
+++ b/jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute4.rdf
@@ -4,6 +4,6 @@
 <rdf:RDF 
     xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
     xmlns="http://example/";>
-  <!-- qnames do not apply to attributes unless ex:attr -->
-  <rdf:Description name="NAME"/>
-</rdf:RDF>
\ No newline at end of file
+  <!-- xml : warning -->
+  <rdf:Description xml:n="NAME"/>
+</rdf:RDF>
diff --git a/jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute2.rdf 
b/jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute5.rdf
similarity index 63%
copy from jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute2.rdf
copy to jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute5.rdf
index 21940b5d63..70da6bddb0 100644
--- a/jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute2.rdf
+++ b/jena-arq/testing/RIOT/rrx-files/bad-unqualified-attribute5.rdf
@@ -4,6 +4,6 @@
 <rdf:RDF 
     xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
     xmlns="http://example/";>
-  <!-- qnames do not apply to attributes unless ex:attr -->
-  <rdf:Description name="NAME"/>
-</rdf:RDF>
\ No newline at end of file
+  <!-- RDF/XML calls out plain attributes startin "xml" : warning -->
+  <rdf:Description XmLname="NAME"/>
+</rdf:RDF>

Reply via email to