001/*
002Copyright (c) 2011+, HL7, Inc
003All rights reserved.
004
005Redistribution and use in source and binary forms, with or without modification, 
006are permitted provided that the following conditions are met:
007
008 * Redistributions of source code must retain the above copyright notice, this 
009   list of conditions and the following disclaimer.
010 * Redistributions in binary form must reproduce the above copyright notice, 
011   this list of conditions and the following disclaimer in the documentation 
012   and/or other materials provided with the distribution.
013 * Neither the name of HL7 nor the names of its contributors may be used to 
014   endorse or promote products derived from this software without specific 
015   prior written permission.
016
017THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
018ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
019WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
020IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
021INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
022NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
023PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
024WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
025ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
026POSSIBILITY OF SUCH DAMAGE.
027
028 */
029package org.hl7.fhir.utilities.xhtml;
030
031/*
032 * #%L
033 * HAPI FHIR - Core Library
034 * %%
035 * Copyright (C) 2014 - 2017 University Health Network
036 * %%
037 * Licensed under the Apache License, Version 2.0 (the "License");
038 * you may not use this file except in compliance with the License.
039 * You may obtain a copy of the License at
040 * 
041 *      http://www.apache.org/licenses/LICENSE-2.0
042 * 
043 * Unless required by applicable law or agreed to in writing, software
044 * distributed under the License is distributed on an "AS IS" BASIS,
045 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
046 * See the License for the specific language governing permissions and
047 * limitations under the License.
048 * #L%
049 */
050
051
052import static org.apache.commons.lang3.StringUtils.isBlank;
053
054import java.util.ArrayList;
055import java.util.HashMap;
056import java.util.List;
057import java.util.Map;
058
059import org.hl7.fhir.instance.model.api.IBaseXhtml;
060
061import ca.uhn.fhir.model.primitive.XhtmlDt;
062
063@ca.uhn.fhir.model.api.annotation.DatatypeDef(name="xhtml")
064public class XhtmlNode implements IBaseXhtml {
065        private static final long serialVersionUID = -4362547161441436492L;
066
067
068        public static class Location {
069                private int line;
070                private int column;
071                public Location(int line, int column) {
072                        super();
073                        this.line = line;
074                        this.column = column;
075                }
076                public int getLine() {
077                        return line;
078                }
079                public int getColumn() {
080                        return column;
081                }
082                @Override
083                public String toString() {
084                        return "Line "+Integer.toString(line)+", column "+Integer.toString(column);
085                }
086        }
087
088        public static final String NBSP = Character.toString((char)0xa0);
089        private static final String DECL_XMLNS = " xmlns=\"http://www.w3.org/1999/xhtml\"";
090
091
092        private Location location;
093        private NodeType nodeType;
094        private String name;
095        private Map<String, String> attributes = new HashMap<String, String>();
096        private List<XhtmlNode> childNodes = new ArrayList<XhtmlNode>();
097        private String content;
098
099        public XhtmlNode() {
100                super();
101        }
102
103
104        public XhtmlNode(NodeType nodeType, String name) {
105                super();
106                this.nodeType = nodeType;
107                this.name = name;
108        }
109
110        public XhtmlNode(NodeType nodeType) {
111                super();
112                this.nodeType = nodeType;
113        }
114
115        public NodeType getNodeType() {
116                return nodeType;
117        }
118
119        public void setNodeType(NodeType nodeType) {
120                this.nodeType = nodeType;
121        }
122
123        public String getName() {
124                return name;
125        }
126
127        public void setName(String name) {
128                assert name.contains(":") == false : "Name should not contain any : but was " + name;
129                this.name = name;
130        }
131
132        public Map<String, String> getAttributes() {
133                return attributes;
134        }
135
136        public List<XhtmlNode> getChildNodes() {
137                return childNodes;
138        }
139
140        public String getContent() {
141                return content;
142        }
143
144        public XhtmlNode setContent(String content) {
145                if (!(nodeType != NodeType.Text || nodeType != NodeType.Comment)) 
146                        throw new Error("Wrong node type");
147                this.content = content;
148                return this;
149        }
150
151        public XhtmlNode addTag(String name)
152        {
153
154                if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 
155                        throw new Error("Wrong node type. is "+nodeType.toString());
156                XhtmlNode node = new XhtmlNode(NodeType.Element);
157                node.setName(name);
158                childNodes.add(node);
159                return node;
160        }
161
162        public XhtmlNode addTag(int index, String name)
163        {
164
165                if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 
166                        throw new Error("Wrong node type. is "+nodeType.toString());
167                XhtmlNode node = new XhtmlNode(NodeType.Element);
168                node.setName(name);
169                childNodes.add(index, node);
170                return node;
171        }
172
173        public XhtmlNode addComment(String content)
174        {
175                if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 
176                        throw new Error("Wrong node type");
177                XhtmlNode node = new XhtmlNode(NodeType.Comment);
178                node.setContent(content);
179                childNodes.add(node);
180                return node;
181        }
182
183        public XhtmlNode addDocType(String content)
184        {
185                if (!(nodeType == NodeType.Document)) 
186                        throw new Error("Wrong node type");
187                XhtmlNode node = new XhtmlNode(NodeType.DocType);
188                node.setContent(content);
189                childNodes.add(node);
190                return node;
191        }
192
193        public XhtmlNode addInstruction(String content)
194        {
195                if (!(nodeType == NodeType.Document)) 
196                        throw new Error("Wrong node type");
197                XhtmlNode node = new XhtmlNode(NodeType.Instruction);
198                node.setContent(content);
199                childNodes.add(node);
200                return node;
201        }
202
203
204
205
206        public XhtmlNode addText(String content)
207        {
208                if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 
209                        throw new Error("Wrong node type");
210                if (content != null) {
211                        XhtmlNode node = new XhtmlNode(NodeType.Text);
212                        node.setContent(content);
213                        childNodes.add(node);
214                        return node;
215                }  
216                return null;
217        }
218
219        public XhtmlNode addText(int index, String content)
220        {
221                if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 
222                        throw new Error("Wrong node type");
223                if (content == null)
224                        throw new Error("Content cannot be null");
225
226                XhtmlNode node = new XhtmlNode(NodeType.Text);
227                node.setContent(content);
228                childNodes.add(index, node);
229                return node;
230        }
231
232        public boolean allChildrenAreText()
233        {
234                boolean res = true;
235                for (XhtmlNode n : childNodes)
236                        res = res && n.getNodeType() == NodeType.Text;
237                return res;
238        }
239
240        public XhtmlNode getElement(String name)
241        {
242                for (XhtmlNode n : childNodes)
243                        if (n.getNodeType() == NodeType.Element && name.equals(n.getName())) 
244                                return n;
245                return null;
246        }
247
248        public XhtmlNode getFirstElement()
249        {
250                for (XhtmlNode n : childNodes)
251                        if (n.getNodeType() == NodeType.Element) 
252                                return n;
253                return null;
254        }
255
256        public String allText() {
257                StringBuilder b = new StringBuilder();
258                for (XhtmlNode n : childNodes)
259                        if (n.getNodeType() == NodeType.Text)
260                                b.append(n.getContent());
261                        else if (n.getNodeType() == NodeType.Element)
262                                b.append(n.allText());
263                return b.toString();
264        }
265
266        public XhtmlNode attribute(String name, String value) {
267                if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 
268                        throw new Error("Wrong node type");
269                if (name == null)
270                        throw new Error("name is null");
271                if (value == null)
272                        throw new Error("value is null");
273                attributes.put(name, value);
274                return this;
275        }
276
277        public boolean hasAttribute(String name) {
278                return getAttributes().containsKey(name);
279        }
280
281        public String getAttribute(String name) {
282                return getAttributes().get(name);
283        }
284
285        public XhtmlNode setAttribute(String name, String value) {
286                getAttributes().put(name, value);
287                return this;    
288        }
289
290        public XhtmlNode copy() {
291                XhtmlNode dst = new XhtmlNode(nodeType);
292                dst.name = name;
293                for (String n : attributes.keySet()) {
294                        dst.attributes.put(n, attributes.get(n));
295                }
296                for (XhtmlNode n : childNodes)
297                        dst.childNodes.add(n.copy());
298                dst.content = content;
299                return dst;
300        }
301
302        @Override
303        public boolean isEmpty() {
304                return (childNodes == null || childNodes.isEmpty()) && content == null;
305        }
306
307        public boolean equalsDeep(XhtmlNode other) {
308                if (other == null) {
309                        return false;
310                }
311
312                if (!(nodeType == other.nodeType) || !compare(name, other.name) || !compare(content, other.content))
313                        return false;
314                if (attributes.size() != other.attributes.size())
315                        return false;
316                for (String an : attributes.keySet())
317                        if (!attributes.get(an).equals(other.attributes.get(an)))
318                                return false;
319                if (childNodes.size() != other.childNodes.size())
320                        return false;
321                for (int i = 0; i < childNodes.size(); i++) {
322                        if (!compareDeep(childNodes.get(i), other.childNodes.get(i)))
323                                return false;
324                }
325                return true;
326        }
327
328        private boolean compare(String s1, String s2) {
329                if (s1 == null && s2 == null)
330                        return true;
331                if (s1 == null || s2 == null)
332                        return false;
333                return s1.equals(s2);
334        }
335
336        private static boolean compareDeep(XhtmlNode e1, XhtmlNode e2) {
337                if (e1 == null && e2 == null)
338                        return true;
339                if (e1 == null || e2 == null)
340                        return false;
341                return e1.equalsDeep(e2);
342        }
343
344        public String getNsDecl() {
345                for (String an : attributes.keySet()) {
346                        if (an.equals("xmlns")) {
347                                return attributes.get(an);
348                        }
349                }
350                return null;
351        }
352
353
354        @Override
355        public String getValueAsString() {
356                if (isEmpty()) {
357                        return null;
358                }
359                try {
360                        String retVal = new XhtmlComposer().compose(this);
361                        retVal = XhtmlDt.preprocessXhtmlNamespaceDeclaration(retVal);
362                        return retVal;
363                } catch (Exception e) {
364                        // TODO: composer shouldn't throw exception like this
365                        throw new RuntimeException(e);
366                }
367        }
368
369        @Override
370        public void setValueAsString(String theValue) throws IllegalArgumentException {
371                this.attributes = null;
372                this.childNodes = null;
373                this.content = null;
374                this.name = null;
375                this.nodeType= null;
376                if (isBlank(theValue)) {
377                        return;
378                }
379
380                String val = theValue.trim();
381
382                if (!val.startsWith("<")) {
383                        val = "<div" + DECL_XMLNS +">" + val + "</div>";
384                }
385                if (val.startsWith("<?") && val.endsWith("?>")) {
386                        return;
387                }
388
389                val = XhtmlDt.preprocessXhtmlNamespaceDeclaration(val);
390
391                try {
392                        // TODO: this is ugly
393                        XhtmlNode fragment = new XhtmlParser().parseFragment(val);
394                        this.attributes = fragment.attributes;
395                        this.childNodes = fragment.childNodes;
396                        this.content = fragment.content;
397                        this.name = fragment.name;
398                        this.nodeType= fragment.nodeType;
399                } catch (Exception e) {
400                        // TODO: composer shouldn't throw exception like this
401                        throw new RuntimeException(e);
402                }
403
404        }
405
406        public XhtmlNode getElementByIndex(int i) {
407                int c = 0;
408                for (XhtmlNode n : childNodes)
409                        if (n.getNodeType() == NodeType.Element) {
410                                if (c == i){
411                                        return n;
412                                }
413                                c++;
414                        }
415                return null;
416        }
417
418        @Override
419        public String getValue() {
420                return getValueAsString();
421        }
422
423        @Override
424        public XhtmlNode setValue(String theValue) throws IllegalArgumentException {
425                setValueAsString(theValue);
426                return this;
427        }
428
429        /**
430         * Returns false
431         */
432        @Override
433        public boolean hasFormatComment() {
434                return false;
435        }
436
437        /**
438         * NOT SUPPORTED - Throws {@link UnsupportedOperationException}
439         */
440        @Override
441        public List<String> getFormatCommentsPre() {
442                throw new UnsupportedOperationException();
443        }
444
445        /**
446         * NOT SUPPORTED - Throws {@link UnsupportedOperationException}
447         */
448        @Override
449        public List<String> getFormatCommentsPost() {
450                throw new UnsupportedOperationException();
451        }
452
453
454        public Location getLocation() {
455                return location;
456        }
457
458
459        public void setLocation(Location location) {
460                this.location = location;
461        }
462
463}