1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.metapath.format;
7   
8   import dev.metaschema.core.metapath.item.node.IAssemblyInstanceGroupedNodeItem;
9   import dev.metaschema.core.metapath.item.node.IAssemblyNodeItem;
10  import dev.metaschema.core.metapath.item.node.IDocumentNodeItem;
11  import dev.metaschema.core.metapath.item.node.IFieldNodeItem;
12  import dev.metaschema.core.metapath.item.node.IFlagNodeItem;
13  import dev.metaschema.core.metapath.item.node.IModelNodeItem;
14  import dev.metaschema.core.metapath.item.node.IModuleNodeItem;
15  import dev.metaschema.core.metapath.item.node.IRootAssemblyNodeItem;
16  import dev.metaschema.core.model.INamedModelInstance;
17  import dev.metaschema.core.model.XmlGroupAsBehavior;
18  import dev.metaschema.core.qname.IEnhancedQName;
19  import dev.metaschema.core.util.ObjectUtils;
20  import edu.umd.cs.findbugs.annotations.NonNull;
21  
22  /**
23   * An {@link IPathFormatter} that produces XPath 3.1 expressions with
24   * namespace-qualified names using the EQName format (e.g.,
25   * {@code Q{namespace}localname}).
26   * <p>
27   * This formatter produces paths suitable for use with XML tooling that requires
28   * namespace qualification. The format follows the XPath 3.1 specification for
29   * EQNames.
30   * <p>
31   * Example output:
32   * {@code /Q{http://example.com}catalog/Q{http://example.com}control[1]/@Q{http://example.com}id}
33   * <p>
34   * For elements with XML grouping ({@link XmlGroupAsBehavior#GROUPED}), the
35   * wrapper element is included in the path with position [1], followed by the
36   * actual element with its position.
37   *
38   * @see IEnhancedQName#toEQName()
39   */
40  public class XPathFormatter implements IPathFormatter {
41  
42    @Override
43    @NonNull
44    public String formatMetaschema(IModuleNodeItem metaschema) {
45      // Returns empty string to produce leading "/" via join in format method
46      return "";
47    }
48  
49    @Override
50    @NonNull
51    public String formatDocument(IDocumentNodeItem document) {
52      // Returns empty string to produce leading "/" via join in format method
53      return "";
54    }
55  
56    @Override
57    @NonNull
58    public String formatRootAssembly(IRootAssemblyNodeItem root) {
59      return ObjectUtils.notNull(root.getQName().toEQName());
60    }
61  
62    @Override
63    @NonNull
64    public String formatAssembly(IAssemblyNodeItem assembly) {
65      return formatModelItem(assembly);
66    }
67  
68    @Override
69    @NonNull
70    public String formatAssembly(IAssemblyInstanceGroupedNodeItem assembly) {
71      return formatModelItem(assembly);
72    }
73  
74    @Override
75    @NonNull
76    public String formatField(IFieldNodeItem field) {
77      return formatModelItem(field);
78    }
79  
80    @Override
81    @NonNull
82    public String formatFlag(IFlagNodeItem flag) {
83      return ObjectUtils.notNull("@" + flag.getQName().toEQName());
84    }
85  
86    /**
87     * Format a model node item (assembly or field) with optional XML grouping
88     * wrapper element.
89     * <p>
90     * When the instance has {@link XmlGroupAsBehavior#GROUPED}, the wrapper element
91     * is prepended to the path with position [1], since the wrapper element appears
92     * exactly once containing all grouped items.
93     *
94     * @param item
95     *          the model node item to format
96     * @return the formatted path segment
97     */
98    @NonNull
99    private static String formatModelItem(@NonNull IModelNodeItem<?, ?> item) {
100     StringBuilder builder = new StringBuilder();
101 
102     // Check for XML grouping wrapper element
103     INamedModelInstance instance = item.getInstance();
104     if (instance != null && instance.getXmlGroupAsBehavior() == XmlGroupAsBehavior.GROUPED) {
105       IEnhancedQName wrapperQName = instance.getEffectiveXmlGroupAsQName();
106       if (wrapperQName != null) {
107         builder.append(wrapperQName.toEQName())
108             .append("[1]/");
109       }
110     }
111 
112     // Append element name with position predicate
113     builder.append(item.getQName().toEQName())
114         .append('[')
115         .append(item.getPosition())
116         .append(']');
117 
118     return ObjectUtils.notNull(builder.toString());
119   }
120 }