1package com.android.server.wifi.hotspot2.omadm;
2
3import org.xml.sax.SAXException;
4
5import java.io.FileNotFoundException;
6import java.io.IOException;
7import java.io.InputStream;
8import java.io.OutputStream;
9import java.nio.charset.StandardCharsets;
10import java.util.*;
11
12public class MOTree {
13    public static final String MgmtTreeTag = "MgmtTree";
14
15    public static final String NodeTag = "Node";
16    public static final String NodeNameTag = "NodeName";
17    public static final String PathTag = "Path";
18    public static final String ValueTag = "Value";
19    public static final String RTPropTag = "RTProperties";
20    public static final String TypeTag = "Type";
21    public static final String DDFNameTag = "DDFName";
22
23    private final String mUrn;
24    private final String mDtdRev;
25    private final OMAConstructed mRoot;
26
27    public MOTree(XMLNode node, String urn) throws IOException, SAXException {
28        Iterator<XMLNode> children = node.getChildren().iterator();
29
30        String dtdRev = null;
31
32        while (children.hasNext()) {
33            XMLNode child = children.next();
34            if (child.getTag().equals(OMAConstants.SyncMLVersionTag)) {
35                dtdRev = child.getText();
36                children.remove();
37                break;
38            }
39        }
40
41        mUrn = urn;
42        mDtdRev = dtdRev;
43
44        mRoot = new ManagementTreeRoot(node, dtdRev);
45
46        for (XMLNode child : node.getChildren()) {
47            buildNode(mRoot, child);
48        }
49    }
50
51    public MOTree(String urn, String rev, OMAConstructed root) {
52        mUrn = urn;
53        mDtdRev = rev;
54        mRoot = root;
55    }
56
57    /**
58     * Build a Passpoint OMA-DM Management Object tree object.
59     * @param urn The URN for the tree.
60     * @param rev The DTD revision for the tree.
61     * @param root The OMA-DM tree root, in all practical cases the PerProviderSubscription
62     *             node.
63     * @return an MOTree object
64     */
65    public static MOTree buildMgmtTree(String urn, String rev, OMAConstructed root) {
66        OMAConstructed realRoot;
67        switch (urn) {
68            case OMAConstants.PPS_URN:
69            case OMAConstants.DevInfoURN:
70            case OMAConstants.DevDetailURN:
71            case OMAConstants.DevDetailXURN:
72                realRoot = new ManagementTreeRoot(OMAConstants.OMAVersion);
73                realRoot.addChild(root);
74                return new MOTree(urn, rev, realRoot);
75            default:
76                return new MOTree(urn, rev, root);
77        }
78    }
79
80    public static boolean hasMgmtTreeTag(String text) {
81        for (int n = 0; n < text.length(); n++) {
82            char ch = text.charAt(n);
83            if (ch > ' ') {
84                return text.regionMatches(true, n, '<' + MgmtTreeTag + '>',
85                        0, MgmtTreeTag.length() + 2);
86            }
87        }
88        return false;
89    }
90
91    private static class NodeData {
92        private final String mName;
93        private String mPath;
94        private String mValue;
95
96        private NodeData(String name) {
97            mName = name;
98        }
99
100        private void setPath(String path) {
101            mPath = path;
102        }
103
104        private void setValue(String value) {
105            mValue = value;
106        }
107
108        public String getName() {
109            return mName;
110        }
111
112        public String getPath() {
113            return mPath;
114        }
115
116        public String getValue() {
117            return mValue;
118        }
119    }
120
121    private static void buildNode(OMANode parent, XMLNode node) throws IOException {
122        if (!node.getTag().equals(NodeTag))
123            throw new IOException("Node is a '" + node.getTag() + "' instead of a 'Node'");
124
125        Map<String, XMLNode> checkMap = new HashMap<>(3);
126        String context = null;
127        List<NodeData> values = new ArrayList<>();
128        List<XMLNode> children = new ArrayList<>();
129
130        NodeData curValue = null;
131
132        for (XMLNode child : node.getChildren()) {
133            XMLNode old = checkMap.put(child.getTag(), child);
134
135            switch (child.getTag()) {
136                case NodeNameTag:
137                    if (curValue != null)
138                        throw new IOException(NodeNameTag + " not expected");
139                    curValue = new NodeData(child.getText());
140
141                    break;
142                case PathTag:
143                    if (curValue == null || curValue.getPath() != null)
144                        throw new IOException(PathTag + " not expected");
145                    curValue.setPath(child.getText());
146
147                    break;
148                case ValueTag:
149                    if (!children.isEmpty())
150                        throw new IOException(ValueTag + " in constructed node");
151                    if (curValue == null || curValue.getValue() != null)
152                        throw new IOException(ValueTag + " not expected");
153                    curValue.setValue(child.getText());
154                    values.add(curValue);
155                    curValue = null;
156
157                    break;
158                case RTPropTag:
159                    if (old != null)
160                        throw new IOException("Duplicate " + RTPropTag);
161                    XMLNode typeNode = getNextNode(child, TypeTag);
162                    XMLNode ddfName = getNextNode(typeNode, DDFNameTag);
163                    context = ddfName.getText();
164                    if (context == null)
165                        throw new IOException("No text in " + DDFNameTag);
166
167                    break;
168                case NodeTag:
169                    if (!values.isEmpty())
170                        throw new IOException("Scalar node " + node.getText() + " has Node child");
171                    children.add(child);
172
173                    break;
174            }
175        }
176
177        if (values.isEmpty()) {
178            if (curValue == null)
179                throw new IOException("Missing name");
180
181            OMANode subNode = parent.addChild(curValue.getName(),
182                    context, null, curValue.getPath());
183
184            for (XMLNode child : children) {
185                buildNode(subNode, child);
186            }
187        } else {
188            if (!children.isEmpty())
189                throw new IOException("Got both sub nodes and value(s)");
190
191            for (NodeData nodeData : values) {
192                parent.addChild(nodeData.getName(), context,
193                        nodeData.getValue(), nodeData.getPath());
194            }
195        }
196    }
197
198    private static XMLNode getNextNode(XMLNode node, String tag) throws IOException {
199        if (node == null)
200            throw new IOException("No node for " + tag);
201        if (node.getChildren().size() != 1)
202            throw new IOException("Expected " + node.getTag() + " to have exactly one child");
203        XMLNode child = node.getChildren().iterator().next();
204        if (!child.getTag().equals(tag))
205            throw new IOException("Expected " + node.getTag() + " to have child '" + tag +
206                    "' instead of '" + child.getTag() + "'");
207        return child;
208    }
209
210    public String getUrn() {
211        return mUrn;
212    }
213
214    public String getDtdRev() {
215        return mDtdRev;
216    }
217
218    public OMAConstructed getRoot() {
219        return mRoot;
220    }
221
222    @Override
223    public String toString() {
224        StringBuilder sb = new StringBuilder();
225        sb.append("MO Tree v").append(mDtdRev).append(", urn ").append(mUrn).append(")\n");
226        sb.append(mRoot);
227
228        return sb.toString();
229    }
230
231    public void marshal(OutputStream out) throws IOException {
232        out.write("tree ".getBytes(StandardCharsets.UTF_8));
233        OMAConstants.serializeString(mDtdRev, out);
234        out.write(String.format("(%s)\n", mUrn).getBytes(StandardCharsets.UTF_8));
235        mRoot.marshal(out, 0);
236    }
237
238    public static MOTree unmarshal(InputStream in) throws IOException {
239        boolean strip = true;
240        StringBuilder tree = new StringBuilder();
241        for (; ; ) {
242            int octet = in.read();
243            if (octet < 0) {
244                throw new FileNotFoundException();
245            } else if (octet > ' ') {
246                tree.append((char) octet);
247                strip = false;
248            } else if (!strip) {
249                break;
250            }
251        }
252        if (!tree.toString().equals("tree")) {
253            throw new IOException("Not a tree: " + tree);
254        }
255
256        String version = OMAConstants.deserializeString(in);
257        int next = in.read();
258        if (next != '(') {
259            throw new IOException("Expected URN in tree definition");
260        }
261        String urn = OMAConstants.readURN(in);
262
263        OMAConstructed root = OMANode.unmarshal(in);
264
265        return new MOTree(urn, version, root);
266    }
267
268    public String toXml() {
269        StringBuilder sb = new StringBuilder();
270        mRoot.toXml(sb);
271        return sb.toString();
272    }
273}
274