001package ca.uhn.fhir.model.primitive;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2017 University Health Network
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 * 
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 * 
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import java.io.StringReader;
024import java.io.StringWriter;
025import java.util.ArrayList;
026import java.util.List;
027
028import javax.xml.stream.FactoryConfigurationError;
029import javax.xml.stream.XMLEventReader;
030import javax.xml.stream.XMLEventWriter;
031import javax.xml.stream.XMLStreamException;
032import javax.xml.stream.events.XMLEvent;
033
034import ca.uhn.fhir.context.ConfigurationException;
035import ca.uhn.fhir.model.api.BasePrimitive;
036import ca.uhn.fhir.model.api.annotation.DatatypeDef;
037import ca.uhn.fhir.model.api.annotation.SimpleSetter;
038import ca.uhn.fhir.parser.DataFormatException;
039import ca.uhn.fhir.util.XmlUtil;
040
041@DatatypeDef(name = "xhtml")
042public class XhtmlDt extends BasePrimitive<List<XMLEvent>> {
043
044        private static final String DECL_XMLNS = " xmlns=\"http://www.w3.org/1999/xhtml\"";
045        private static final String DIV_OPEN_FIRST = "<div" + DECL_XMLNS + ">";
046        private static final long serialVersionUID = 1L;
047
048        /**
049         * Constructor
050         */
051        public XhtmlDt() {
052                // nothing
053        }
054
055        /**
056         * Constructor which accepts a string code
057         * 
058         * @see #setValueAsString(String) for a description of how this value is applied
059         */
060        @SimpleSetter()
061        public XhtmlDt(@SimpleSetter.Parameter(name = "theTextDiv") String theTextDiv) {
062                setValueAsString(theTextDiv);
063        }
064
065        @Override
066        protected String encode(List<XMLEvent> theValue) {
067                try {
068                        StringWriter w = new StringWriter();
069                        XMLEventWriter ew = XmlUtil.createXmlFragmentWriter(w);
070
071                        for (XMLEvent next : getValue()) {
072                                if (next.isCharacters()) {
073                                        ew.add(next);
074                                } else {
075                                        ew.add(next);
076                                }
077                        }
078                        ew.close();
079                        return w.toString();
080                } catch (XMLStreamException e) {
081                        throw new DataFormatException("Problem with the contained XML events", e);
082                } catch (FactoryConfigurationError e) {
083                        throw new ConfigurationException(e);
084                }
085        }
086
087        public boolean hasContent() {
088                return getValue() != null && getValue().size() > 0;
089        }
090
091        @Override
092        public boolean isEmpty() {
093                return super.isBaseEmpty() && (getValue() == null || getValue().isEmpty());
094        }
095
096        @Override
097        protected List<XMLEvent> parse(String theValue) {
098                String val = theValue.trim();
099                if (!val.startsWith("<")) {
100                        val = DIV_OPEN_FIRST + val + "</div>";
101                }
102                boolean hasProcessingInstruction = val.startsWith("<?");
103                if (hasProcessingInstruction && val.endsWith("?>")) {
104                        return null;
105                }
106
107                
108                try {
109                        ArrayList<XMLEvent> value = new ArrayList<XMLEvent>();
110                        StringReader reader = new StringReader(val);
111                        XMLEventReader er = XmlUtil.createXmlReader(reader);
112                        boolean first = true;
113                        while (er.hasNext()) {
114                                XMLEvent next = er.nextEvent();
115                                if (first) {
116                                        first = false;
117                                        continue;
118                                }
119                                if (er.hasNext()) {
120                                        // don't add the last event
121                                        value.add(next);
122                                }
123                        }
124                        return value;
125
126                } catch (XMLStreamException e) {
127                        throw new DataFormatException("String does not appear to be valid XML/XHTML (error is \"" + e.getMessage() + "\"): " + theValue, e);
128                } catch (FactoryConfigurationError e) {
129                        throw new ConfigurationException(e);
130                }
131        }
132
133        /**
134         * Accepts a textual DIV and parses it into XHTML events which are stored internally.
135         * <p>
136         * <b>Formatting note:</b> The text will be trimmed {@link String#trim()}. If the text does not start with an HTML tag (generally this would be a div tag), a div tag will be automatically placed
137         * surrounding the text.
138         * </p>
139         * <p>
140         * Also note that if the parsed text contains any entities (&amp;foo;) which are not a part of the entities defined in core XML (e.g. &amp;sect; which is valid in XHTML 1.0 but not in XML 1.0) they
141         * will be parsed and converted to their equivalent unicode character.
142         * </p>
143         */
144        @Override
145        public void setValueAsString(String theValue) throws DataFormatException {
146                if (theValue == null || theValue.isEmpty()) {
147                        super.setValueAsString(null);
148                } else {
149                        String value = theValue.trim();
150                        value = preprocessXhtmlNamespaceDeclaration(value);
151
152                        super.setValueAsString(value);
153                }
154        }
155
156        public static String preprocessXhtmlNamespaceDeclaration(String value) {
157                if (value.charAt(0) != '<') {
158                        value = DIV_OPEN_FIRST + value + "</div>";
159                }
160                
161                boolean hasProcessingInstruction = value.startsWith("<?");
162                int firstTagIndex = value.indexOf("<", hasProcessingInstruction ? 1 : 0);
163                if (firstTagIndex != -1) {
164                        int firstTagEnd = value.indexOf(">", firstTagIndex);
165                        int firstSlash = value.indexOf("/", firstTagIndex);
166                        if (firstTagEnd != -1) {
167                                if (firstSlash > firstTagEnd) {
168                                        String firstTag = value.substring(firstTagIndex, firstTagEnd);
169                                        if (!firstTag.contains(" xmlns")) {
170                                                value = value.substring(0, firstTagEnd) + DECL_XMLNS + value.substring(firstTagEnd);
171                                        }
172                                }
173                        }
174                }
175                return value;
176        }
177
178}