View Javadoc
1   /*
2    * Copyright 2009 the original author or authors.
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *     
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.openehealth.ipf.commons.ihe.ws.cxf.payload;
17  
18  import java.util.HashMap;
19  import java.util.Map;
20  import java.util.regex.Matcher;
21  import java.util.regex.Pattern;
22  
23  import javax.xml.XMLConstants;
24  
25  import org.apache.cxf.wsdl.interceptors.DocLiteralInInterceptor;
26  import org.apache.cxf.interceptor.Fault;
27  import org.apache.cxf.message.Message;
28  import org.apache.cxf.phase.AbstractPhaseInterceptor;
29  import org.apache.cxf.phase.Phase;
30  import org.w3c.dom.Document;
31  import org.w3c.dom.Element;
32  import org.w3c.dom.NamedNodeMap;
33  import org.w3c.dom.Node;
34  
35  import static org.openehealth.ipf.commons.ihe.ws.cxf.payload.StringPayloadHolder.PayloadType.SOAP_BODY;
36  
37  /**
38   * CXF interceptor which inserts XML namespace declarations from incoming 
39   * SOAP Envelope and SOAP Body elements into the String payload.
40   * 
41   * @author Dmytro Rud
42   */
43  public class InNamespaceMergeInterceptor extends AbstractPhaseInterceptor<Message> {
44  
45      private final static Pattern XMLNS_PATTERN = Pattern.compile("xmlns:(\\w+)=\".+?\"");
46  
47      public InNamespaceMergeInterceptor() {
48          super(Phase.UNMARSHAL);
49          addAfter(DocLiteralInInterceptor.class.getName());
50      }
51  
52      @Override
53      public void handleMessage(Message message) throws Fault {
54          if (isGET(message)) {
55              return;
56          }
57  
58          StringPayloadHolder payloadHolder = message.getContent(StringPayloadHolder.class);
59          if (payloadHolder != null) {
60              String payload = payloadHolder.get(SOAP_BODY);
61              if (isXmlContent(payload)) {
62                  Document document = (Document) message.getContent(Node.class);
63                  if (document != null) {
64                      // The Node representation of the message is usually produced
65                      // by CXF's ReadHeadersInterceptor, but this does not happen when the message
66                      // does not contain SOAP headers (i.e. when the request is invalid, because
67                      // it does not fulfill the IHE requirement of using WS-Addressing).
68                      // Let us hope that in this unhappy case all relevant XML namespaces
69                      // are defined directly in the SOAP body and not in the SOAP envelope.
70                      payloadHolder.put(SOAP_BODY, enrichNamespaces(document, payload));
71                  }
72              }
73          }
74      }
75  
76      
77      /**
78       * Returns <code>true</code> iff the given payload string seems to contain XML.
79       */
80      private static boolean isXmlContent(String payload) {
81          if (payload != null) {
82              for (int i = 0; i < payload.length(); ++i) {
83                  char c = payload.charAt(i);
84                  if (!Character.isWhitespace(c)) {
85                      return (c == '<');
86                  }
87              }
88          }
89          return false;
90      }
91      
92      
93      /**
94       * Copies namespace definitions from SOAP Envelope and SOAP Body elements 
95       * of the given XML Document into the top-level element of the XML document
96       * represented by the given String.
97       * <p>
98       * Caller is supposed to take care of the parameters' correctness,
99       * so there is no sophisticated error handling. 
100      * 
101      * @param source
102      *      source SOAP document as a DOM object.
103      * @param target
104      *      target XML Document as String.
105      * @return target 
106      *      XML document enriched with namespace declarations from the
107      *      source XML document.
108      */
109     protected static String enrichNamespaces(Document source, String target) {
110         Map<String, String> namespaces = new HashMap<>();
111 
112         // collect namespace definitions from <soap:Envelope>
113         Element envelope = source.getDocumentElement();
114         addNamespacesFromElement(envelope, namespaces);
115 
116         // collect namespace definitions from <soap:Body>
117         Node node = envelope.getFirstChild();
118         while ((node != null) && !((node instanceof Element) && "Body".equals(node.getLocalName()))) {
119             node = node.getNextSibling();
120         }
121         Element body = (Element) node;
122         addNamespacesFromElement(body, namespaces);
123 
124         if (!namespaces.isEmpty()) {
125             // determine boundaries of the body's root element, ignore leading comments
126             int startPos = 0;
127             int endPos = target.indexOf('>');
128             while ((endPos != -1) && "--".equals(target.substring(endPos - 2, endPos))) {
129                 startPos = endPos + 1;
130                 endPos = target.indexOf('>', startPos);
131             }
132             if (endPos == -1) {
133                 return target;
134             }
135             if (target.charAt(endPos - 1) == '/') {
136                 --endPos;
137             }
138 
139             // ignore namespaces which are (re)defined in the payload
140             String startTag = target.substring(startPos, endPos);
141             Matcher matcher = XMLNS_PATTERN.matcher(startTag);
142             while (matcher.find()) {
143                 namespaces.remove(matcher.group(1));
144             }
145 
146             // insert remained definitions (if any)
147             if (!namespaces.isEmpty()) {
148                 StringBuilder sb = new StringBuilder(startTag);
149                 for (Map.Entry<String, String> ns : namespaces.entrySet()) {
150                     sb.append(" xmlns:").append(ns.getKey()).append("=\"")
151                       .append(ns.getValue()).append('"');
152                 }
153                 sb.append(target.substring(endPos));
154                 return sb.toString();
155             }
156         }
157 
158         return target;
159     }
160 
161     
162     /**
163      * Adds NS defined in the given XML element to the map.
164      * <p>
165      * Existing map items will be overwritten, so this method should be called
166      * in the tree-descending order.
167      */
168     protected static void addNamespacesFromElement(Element elem, Map<String, String> map) {
169         NamedNodeMap attributes = elem.getAttributes();
170         for (int i = 0; i < attributes.getLength(); ++i) {
171             Node attribute = attributes.item(i);
172             if (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(attribute.getNamespaceURI())) {
173                 map.put(attribute.getLocalName(), attribute.getTextContent());
174             }
175         }
176     }
177 
178 }