1/*
2 * Copyright 2009 Mike Cumings
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
17package com.kenai.jbosh;
18
19import java.util.Collections;
20import java.util.HashMap;
21import java.util.Map;
22import java.util.concurrent.atomic.AtomicReference;
23import java.util.regex.Matcher;
24import java.util.regex.Pattern;
25import javax.xml.XMLConstants;
26
27/**
28 * Implementation of the {@code AbstractBody} class which allows for the
29 * definition of  messages from individual elements of a body.
30 * <p/>
31 * A message is constructed by creating a builder, manipulating the
32 * configuration of the builder, and then building it into a class instance,
33 * as in the following example:
34 * <pre>
35 * ComposableBody body = ComposableBody.builder()
36 *     .setNamespaceDefinition("foo", "http://foo.com/bar")
37 *     .setPayloadXML("<foo:data>Data to send to remote server</foo:data>")
38 *     .build();
39 * </pre>
40 * Class instances can also be "rebuilt", allowing them to be used as templates
41 * when building many similar messages:
42 * <pre>
43 * ComposableBody body2 = body.rebuild()
44 *     .setPayloadXML("<foo:data>More data to send</foo:data>")
45 *     .build();
46 * </pre>
47 * This class does only minimal syntactic and semantic checking with respect
48 * to what the generated XML will look like.  It is up to the developer to
49 * protect against the definition of malformed XML messages when building
50 * instances of this class.
51 * <p/>
52 * Instances of this class are immutable and thread-safe.
53 */
54public final class ComposableBody extends AbstractBody {
55
56    /**
57     * Pattern used to identify the beginning {@code body} element of a
58     * BOSH message.
59     */
60    private static final Pattern BOSH_START =
61            Pattern.compile("<" + "(?:(?:[^:\t\n\r >]+:)|(?:\\{[^\\}>]*?}))?"
62            + "body" + "(?:[\t\n\r ][^>]*?)?" + "(/>|>)");
63
64    /**
65     * Map of all attributes to their values.
66     */
67    private final Map<BodyQName, String> attrs;
68
69    /**
70     * Payload XML.
71     */
72    private final String payload;
73
74    /**
75     * Computed raw XML.
76     */
77    private final AtomicReference<String> computed =
78            new AtomicReference<String>();
79
80    /**
81     * Class instance builder, after the builder pattern.  This allows each
82     * message instance to be immutable while providing flexibility when
83     * building new messages.
84     * <p/>
85     * Instances of this class are <b>not</b> thread-safe.
86     */
87    public static final class Builder {
88        private Map<BodyQName, String> map;
89        private boolean doMapCopy;
90        private String payloadXML;
91
92        /**
93         * Prevent direct construction.
94         */
95        private Builder() {
96            // Empty
97        }
98
99        /**
100         * Creates a builder which is initialized to the values of the
101         * provided {@code ComposableBody} instance.  This allows an
102         * existing {@code ComposableBody} to be used as a
103         * template/starting point.
104         *
105         * @param source body template
106         * @return builder instance
107         */
108        private static Builder fromBody(final ComposableBody source) {
109            Builder result = new Builder();
110            result.map = source.getAttributes();
111            result.doMapCopy = true;
112            result.payloadXML = source.payload;
113            return result;
114        }
115
116        /**
117         * Set the body message's wrapped payload content.  Any previous
118         * content will be replaced.
119         *
120         * @param xml payload XML content
121         * @return builder instance
122         */
123        public Builder setPayloadXML(final String xml) {
124            if (xml == null) {
125                throw(new IllegalArgumentException(
126                        "payload XML argument cannot be null"));
127            }
128            payloadXML = xml;
129            return this;
130        }
131
132        /**
133         * Set an attribute on the message body / wrapper element.
134         *
135         * @param name qualified name of the attribute
136         * @param value value of the attribute
137         * @return builder instance
138         */
139        public Builder setAttribute(
140                final BodyQName name, final String value) {
141            if (map == null) {
142                map = new HashMap<BodyQName, String>();
143            } else if (doMapCopy) {
144                map = new HashMap<BodyQName, String>(map);
145                doMapCopy = false;
146            }
147            if (value == null) {
148                map.remove(name);
149            } else {
150                map.put(name, value);
151            }
152            return this;
153        }
154
155        /**
156         * Convenience method to set a namespace definition. This would result
157         * in a namespace prefix definition similar to:
158         * {@code <body xmlns:prefix="uri"/>}
159         *
160         * @param prefix prefix to define
161         * @param uri namespace URI to associate with the prefix
162         * @return builder instance
163         */
164        public Builder setNamespaceDefinition(
165                final String prefix, final String uri) {
166            BodyQName qname = BodyQName.createWithPrefix(
167                    XMLConstants.XML_NS_URI, prefix,
168                    XMLConstants.XMLNS_ATTRIBUTE);
169            return setAttribute(qname, uri);
170        }
171
172        /**
173         * Build the immutable object instance with the current configuration.
174         *
175         * @return composable body instance
176         */
177        public ComposableBody build() {
178            if (map == null) {
179                map = new HashMap<BodyQName, String>();
180            }
181            if (payloadXML == null) {
182                payloadXML = "";
183            }
184            return new ComposableBody(map, payloadXML);
185        }
186    }
187
188    ///////////////////////////////////////////////////////////////////////////
189    // Constructors:
190
191    /**
192     * Prevent direct construction.  This constructor is for body messages
193     * which are dynamically assembled.
194     */
195    private ComposableBody(
196            final Map<BodyQName, String> attrMap,
197            final String payloadXML) {
198        super();
199        attrs = attrMap;
200        payload = payloadXML;
201    }
202
203    /**
204     * Parse a static body instance into a composable instance.  This is an
205     * expensive operation and should not be used lightly.
206     * <p/>
207     * The current implementation does not obtain the payload XML by means of
208     * a proper XML parser.  It uses some string pattern searching to find the
209     * first @{code body} element and the last element's closing tag.  It is
210     * assumed that the static body's XML is well formed, etc..  This
211     * implementation may change in the future.
212     *
213     * @param body static body instance to convert
214     * @return composable bosy instance
215     * @throws BOSHException
216     */
217    static ComposableBody fromStaticBody(final StaticBody body)
218    throws BOSHException {
219        String raw = body.toXML();
220        Matcher matcher = BOSH_START.matcher(raw);
221        if (!matcher.find()) {
222            throw(new BOSHException(
223                    "Could not locate 'body' element in XML.  The raw XML did"
224                    + " not match the pattern: " + BOSH_START));
225        }
226        String payload;
227        if (">".equals(matcher.group(1))) {
228            int first = matcher.end();
229            int last = raw.lastIndexOf("</");
230            if (last < first) {
231                last = first;
232            }
233            payload = raw.substring(first, last);
234        } else {
235            payload = "";
236        }
237
238        return new ComposableBody(body.getAttributes(), payload);
239    }
240
241    /**
242     * Create a builder instance to build new instances of this class.
243     *
244     * @return AbstractBody instance
245     */
246    public static Builder builder() {
247        return new Builder();
248    }
249
250    /**
251     * If this {@code ComposableBody} instance is a dynamic instance, uses this
252     * {@code ComposableBody} instance as a starting point, create a builder
253     * which can be used to create another {@code ComposableBody} instance
254     * based on this one. This allows a {@code ComposableBody} instance to be
255     * used as a template.  Note that the use of the returned builder in no
256     * way modifies or manipulates the current {@code ComposableBody} instance.
257     *
258     * @return builder instance which can be used to build similar
259     *  {@code ComposableBody} instances
260     */
261    public Builder rebuild() {
262        return Builder.fromBody(this);
263    }
264
265    ///////////////////////////////////////////////////////////////////////////
266    // Accessors:
267
268    /**
269     * {@inheritDoc}
270     */
271    public Map<BodyQName, String> getAttributes() {
272        return Collections.unmodifiableMap(attrs);
273    }
274
275    /**
276     * {@inheritDoc}
277     */
278    public String toXML() {
279        String comp = computed.get();
280        if (comp == null) {
281            comp = computeXML();
282            computed.set(comp);
283        }
284        return comp;
285    }
286
287    /**
288     * Get the paylaod XML in String form.
289     *
290     * @return payload XML
291     */
292    public String getPayloadXML() {
293        return payload;
294    }
295
296    ///////////////////////////////////////////////////////////////////////////
297    // Private methods:
298
299    /**
300     * Escape the value of an attribute to ensure we maintain valid
301     * XML syntax.
302     *
303     * @param value value to escape
304     * @return escaped value
305     */
306    private String escape(final String value) {
307        return value.replace("'", "&apos;");
308    }
309
310    /**
311     * Generate a String representation of the message body.
312     *
313     * @return XML string representation of the body
314     */
315    private String computeXML() {
316        BodyQName bodyName = getBodyQName();
317        StringBuilder builder = new StringBuilder();
318        builder.append("<");
319        builder.append(bodyName.getLocalPart());
320        for (Map.Entry<BodyQName, String> entry : attrs.entrySet()) {
321            builder.append(" ");
322            BodyQName name = entry.getKey();
323            String prefix = name.getPrefix();
324            if (prefix != null && prefix.length() > 0) {
325                builder.append(prefix);
326                builder.append(":");
327            }
328            builder.append(name.getLocalPart());
329            builder.append("='");
330            builder.append(escape(entry.getValue()));
331            builder.append("'");
332        }
333        builder.append(" ");
334        builder.append(XMLConstants.XMLNS_ATTRIBUTE);
335        builder.append("='");
336        builder.append(bodyName.getNamespaceURI());
337        builder.append("'>");
338        if (payload != null) {
339            builder.append(payload);
340        }
341        builder.append("</body>");
342        return builder.toString();
343    }
344
345}
346