001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.transformation;
018:
019: import java.io.IOException;
020: import java.io.OutputStream;
021: import java.util.Map;
022:
023: import org.apache.avalon.framework.configuration.Configuration;
024: import org.apache.avalon.framework.configuration.ConfigurationException;
025: import org.apache.avalon.framework.parameters.Parameters;
026: import org.apache.avalon.framework.service.ServiceException;
027: import org.apache.avalon.framework.service.ServiceManager;
028: import org.apache.avalon.framework.service.ServiceSelector;
029: import org.apache.cocoon.ProcessingException;
030: import org.apache.cocoon.components.source.SourceUtil;
031: import org.apache.cocoon.environment.SourceResolver;
032: import org.apache.cocoon.serialization.Serializer;
033: import org.apache.cocoon.xml.XMLUtils;
034: import org.apache.cocoon.xml.dom.DOMStreamer;
035: import org.apache.cocoon.xml.dom.DOMUtil;
036: import org.apache.excalibur.source.ModifiableSource;
037: import org.apache.excalibur.source.Source;
038: import org.apache.excalibur.source.SourceException;
039: import org.apache.excalibur.xml.dom.DOMParser;
040: import org.apache.excalibur.xml.xpath.XPathProcessor;
041: import org.w3c.dom.DOMException;
042: import org.w3c.dom.Document;
043: import org.w3c.dom.DocumentFragment;
044: import org.w3c.dom.Node;
045: import org.w3c.dom.NodeList;
046: import org.xml.sax.Attributes;
047: import org.xml.sax.SAXException;
048:
049: /**
050: * @cocoon.sitemap.component.documentation
051: * This transformer allows you to output to a ModifiableSource.
052: *
053: * @cocoon.sitemap.component.name sourcewriting
054: * @cocoon.sitemap.component.logger sitemap.transformer.write-source
055: *
056: * This transformer allows you to output to a ModifiableSource.
057: *
058: * <p>Definition:</p>
059: * <pre>
060: * <map:transformer name="tofile" src="org.apache.cocoon.transformation.SourceWritingTransformer">
061: * <!-- 'xml' is the default Serializer (if your Source needs one, like for instance FileSource) -->
062: * <map:parameter name="serializer" value="xml"/>
063: * </map:transformer/>
064: * </pre>
065: *
066: * <p>Invocation:</p>
067: * <pre>
068: * <map:transform type="tofile">
069: * <map:parameter name="serializer" value="xml"/> <!-- you can optionally override the serializer here -->
070: * </map:transform>
071: * </pre>
072: *
073: * <p>The Tags:</p>
074: * <pre>
075: * <source:write create="[true]|false"> - replaces the entire content of an existing asset, if @create is 'true' (default), a new asset will be created if one does not already exist.
076: * <source:source>The System ID of the asset to be written to</source:source> - eg: "docs/blah.xml" or "context://blah.xml" etc.
077: * <source:path>[Optional] XPath to specify how your content is wrapped</source:path> - eg: "doc" (your content is placed inside a <doc/> root tag). NOTE: if this value is omitted, your content MUST have only ONE top-level node.
078: * <source:fragment>The XML Fragment to be written</source:fragment> - eg: "<foo><bar id="dogcow"/></foo>" or "<foo/><bar><dogcow/><bar/>" etc. NOTE: the second example type, can only be used when the <source:path/> tag has been specified.
079: * <source:write>
080: *
081: * <source:insert create="[true]|false" overwrite="[true]|false"> - inserts content into an existing asset, if @create is 'true' (default), a new asset will be created if one does not already exist. If @overwrite is set to 'true' the data is only inserted if the node specified by the 'replacePath' does not exists.
082: * <source:source>The System ID of the asset to be written to</source:source> - eg: "docs/blah.xml" or "context://blah.xml" etc.
083: * <source:path>XPath specifying the node into which the content is inserted</source:path> - eg: "doc" (your content is appended as the last child of the <doc/> root tag), or "doc/section[3]". NOTE: this tag is required in <source:insert/> unlike <source:write/> where it is optional.
084: * <source:replace>[Optional] XPath (relative to <source:path/>) to the node that is replaced by your new content</source:replace> - eg: "foo/bar/dogcow/@status='cut'" (is equivalent to this in XSLT: select="foo[bar/dogcow/@status='cut']").
085: * <source:reinsert>[Optional] The XPath (relative to <source:replace/>) to backup the overwritten node to</source:reinsert> - eg: "foo/versions" or "/doc/versions/foo". NOTE: If specified and a node is replaced, all children of this replaced node will be reinserted at the given path.
086: * <source:fragment>The XML Fragment to be written</source:fragment> - eg: "<foo><bar id="dogcow"/></foo>" or "<foo/><bar><dogcow/><bar/>" etc.
087: * <source:insert>
088: *
089: * <source:delete > - deletes an existing asset.
090: * <source:source>The System ID of the asset to be deleted</source:source> - eg: "docs/blah.xml" or "context://blah.xml" etc.
091: * <source:path>[Ignored] XPath to specify how your content is wrapped</source:path>
092: * <source:fragment>[Ignored]The XML Fragment to be written</source:fragment>
093: * <source:delete>
094: * </pre>
095: *
096: *
097: * <p>Input XML document example (write):</p>
098: * <pre>
099: * <page>
100: * ...
101: * <source:write xmlns:source="http://apache.org/cocoon/source/1.0">
102: * <source:source>context://doc/editable/my.xml</source:source>
103: * <source:fragment><page>
104: * <title>Hello World</title>
105: * <content>
106: * <p>This is my first paragraph.</p>
107: * </content>
108: * </page></source:fragment>
109: * </source:write>
110: * ...
111: * </page>
112: * </pre>
113: *
114: * <p>Input XML document example (insert at end):</p>
115: * <pre>
116: * <page>
117: * ...
118: * <source:insert xmlns:source="http://apache.org/cocoon/source/1.0">
119: * <source:source>context://doc/editable/my.xml</source:source>
120: * <source:path>page/content</source:path>
121: * <source:fragment>
122: * <p>This paragraph gets <emp>inserted</emp>.</p>
123: * <p>With this one, at the end of the content.</p>
124: * </source:fragment>
125: * </source:insert>
126: * ...
127: * </page>
128: * </pre>
129: *
130: * <p>Input XML document example (insert at beginning):</p>
131: * <pre>
132: * <page>
133: * ...
134: * <source:insert>
135: * <source:source>context://doc/editable/my.xml</source:source>
136: * <source:path>page</source:path>
137: * <source:replace>content</source:replace>
138: * <source:reinsert>content</source:reinsert>
139: * <source:fragment>
140: * <content>
141: * <p>This new paragraph gets inserted <emp>before</emp> the other ones.</p>
142: * </content>
143: * </source:fragment>
144: * <source:insert>
145: * ...
146: * </page>
147: * </pre>
148: *
149: * <p>Input XML document example (replace):</p>
150: * <pre>
151: * <page>
152: * ...
153: * <source:insert xmlns:source="http://apache.org/cocoon/source/1.0">
154: * <source:source>context://doc/editable/my.xml"</source:source>
155: * <source:path>page/content</source:path>
156: * <source:replace>p[1]</source:replace>
157: * <source:fragment>
158: * <p>This paragraph <emp>replaces</emp> the first paragraph.</p>
159: * </source:fragment>
160: * </source:insert>
161: * ...
162: * </page>
163: * </pre>
164: *
165: * <p>Output XML document example:</p>
166: * <pre>
167: * <page>
168: * ...
169: * <sourceResult xmlns:source="http://apache.org/cocoon/source/1.0">
170: * <action>new|overwritten|none</action>
171: * <behaviour>write|insert<behaviour>
172: * <execution>success|failure</execution>
173: * <serializer>xml</serializer>
174: * <source>file:/source/specific/path/to/context/doc/editable/my.xml</source>
175: * </sourceResult>
176: * ...
177: * </page>
178: * </pre>
179: *
180: *
181: * The XPath specification is very complicated. So here is an example for the sitemap:
182: * <pre>
183: * <page xmlns:source="http://apache.org/cocoon/source/1.0">
184: * ...
185: * <source:insert>
186: * <source:source>sitemap.xmap</source:source>
187: * <source:path>/*[namespace-uri()="http://apache.org/cocoon/sitemap/1.0" and local-name()="sitemap"]/*[namespace-uri()="http://apache.org/cocoon/sitemap/1.0" and local-name()="components"]/*[namespace-uri()="http://apache.org/cocoon/sitemap/1.0" and local-name()="generators"]</source:path>
188: * <source:fragment>
189: * <generator name="file" xmln="http://apache.org/cocoon/sitemap/1.0">
190: * <test/>
191: * </generator>
192: * </source:fragment>
193: * <source:replace>*[namespace-uri()="http://apache.org/cocoon/sitemap/1.0" and local-name()="generator" and attribute::name="file"]</source:replace>
194: * </source:insert>
195: * ...
196: * </page>
197: * </pre>
198: *
199: * <p>This insert replaces (if it exists) the file generator definition with a new one.
200: * As the sitemap uses namespaces the XPath for the generator is rather complicated.
201: * Due to this it is necessary that the node specified by path exists if namespaces
202: * are used! Otherwise a node with the name * would be created...</p>
203: *
204: * <p>The create attribute of insert. If this is set
205: * to true (default is true), the file is created if it does not exists.
206: * If it is set to false, it is not created, making insert a real insert.
207: * create is only usable for files!</p>
208: * <p>In addition the overwrite attribute is used to check if replacing is allowed.
209: * If overwrite is true (the default) the node is replaced. If it is false
210: * the node is not inserted if the replace node is available.</p>
211: *
212: * <p>[JQ] - the way I understand this, looking at the code:
213: * <pre>
214: * if 'replace' is not specified, your 'fragment' is appended as a child of 'path'.
215: * if 'replace' is specified and it exists and 'overwrite' is true, your 'fragment' is inserted in 'path', before 'replace' and then 'replace' is deleted.
216: * if 'replace' is specified and it exists and 'overwrite' is false, no action occurs.
217: * if 'replace' is specified and it does not exist and 'overwrite' is true, your 'fragment' is appended as a child of 'path'.
218: * if 'replace' is specified and it does not exist and 'overwrite' is false, your 'fragment' is appended as a child of 'path'.
219: * if 'reinsert' is specified and it does not exist, no action occurs.
220: * </pre></p>
221: *
222: * The <source:reinsert> option can be used to
223: * reinsert a replaced node at a given path in the new fragment.
224: *
225: * <b>
226: * TODO: Use the serializer instead of the XMLUtils for inserting of fragments<br/>
227: * TODO: Add a <source:before/> tag.
228: * </b>
229: *
230: * @author <a href="mailto:cziegeler@s-und-n.de">Carsten Ziegeler</a>
231: * @author <a href="mailto:jeremy@apache.org">Jeremy Quinn</a>
232: * @author <a href="mailto:gianugo@apache.org">Gianugo Rabellino</a>
233: * @version $Id: SourceWritingTransformer.java 433543 2006-08-22 06:22:54Z crossley $
234: */
235: public class SourceWritingTransformer extends AbstractSAXTransformer {
236:
237: public static final String SWT_URI = "http://apache.org/cocoon/source/1.0";
238: public static final String DEFAULT_SERIALIZER = "xml";
239:
240: /** incoming elements */
241: public static final String WRITE_ELEMENT = "write";
242: public static final String INSERT_ELEMENT = "insert";
243: public static final String PATH_ELEMENT = "path";
244: public static final String FRAGMENT_ELEMENT = "fragment";
245: public static final String REPLACE_ELEMENT = "replace";
246: public static final String DELETE_ELEMENT = "delete";
247: public static final String SOURCE_ELEMENT = "source";
248: public static final String REINSERT_ELEMENT = "reinsert";
249: /** outgoing elements */
250: public static final String RESULT_ELEMENT = "sourceResult";
251: public static final String EXECUTION_ELEMENT = "execution";
252: public static final String BEHAVIOUR_ELEMENT = "behaviour";
253: public static final String ACTION_ELEMENT = "action";
254: public static final String MESSAGE_ELEMENT = "message";
255: public static final String SERIALIZER_ELEMENT = "serializer";
256: /** main (write or insert) tag attributes */
257: public static final String SERIALIZER_ATTRIBUTE = "serializer";
258: public static final String CREATE_ATTRIBUTE = "create";
259: public static final String OVERWRITE_ATTRIBUTE = "overwrite";
260: /** results */
261: public static final String RESULT_FAILED = "failed";
262: public static final String RESULT_SUCCESS = "success";
263: public static final String ACTION_NONE = "none";
264: public static final String ACTION_NEW = "new";
265: public static final String ACTION_OVER = "overwritten";
266: public static final String ACTION_DELETE = "deleted";
267: /** The current state */
268: private static final int STATE_OUTSIDE = 0;
269: private static final int STATE_INSERT = 1;
270: private static final int STATE_PATH = 3;
271: private static final int STATE_FRAGMENT = 4;
272: private static final int STATE_REPLACE = 5;
273: private static final int STATE_FILE = 6;
274: private static final int STATE_REINSERT = 7;
275: private static final int STATE_WRITE = 8;
276: private static final int STATE_DELETE = 9;
277: private int state;
278: private int parent_state;
279:
280: /** The configured serializer name */
281: protected String configuredSerializerName;
282:
283: /** The XPath processor */
284: protected XPathProcessor xpathProcessor;
285:
286: /**
287: * Constructor. Set the namespace.
288: */
289: public SourceWritingTransformer() {
290: this .defaultNamespaceURI = SWT_URI;
291: }
292:
293: /**
294: * Get the current <code>Configuration</code> instance used by this
295: * <code>Configurable</code>.
296: */
297: public void configure(Configuration configuration)
298: throws ConfigurationException {
299: super .configure(configuration);
300: this .configuredSerializerName = configuration.getChild(
301: SERIALIZER_ATTRIBUTE).getValue(DEFAULT_SERIALIZER);
302: }
303:
304: /**
305: * Get the <code>Parameter</code> called "serializer" from the
306: * <code>Transformer</code> invocation.
307: */
308: public void setup(SourceResolver resolver, Map objectModel,
309: String src, Parameters par) throws ProcessingException,
310: SAXException, IOException {
311: super .setup(resolver, objectModel, src, par);
312:
313: this .configuredSerializerName = par.getParameter(
314: SERIALIZER_ATTRIBUTE, this .configuredSerializerName);
315: this .state = STATE_OUTSIDE;
316: }
317:
318: /**
319: * Receive notification of the beginning of an element.
320: *
321: * @param uri The Namespace URI, or the empty string if the element has no
322: * Namespace URI or if Namespace
323: * processing is not being performed.
324: * @param name The local name (without prefix), or the empty string if
325: * Namespace processing is not being performed.
326: * @param raw The raw XML 1.0 name (with prefix), or the empty string if
327: * raw names are not available.
328: * @param attr The attributes attached to the element. If there are no
329: * attributes, it shall be an empty Attributes object.
330: */
331: public void startTransformingElement(String uri, String name,
332: String raw, Attributes attr) throws SAXException,
333: IOException, ProcessingException {
334: if (getLogger().isDebugEnabled()) {
335: getLogger().debug(
336: "Start transforming element. uri=" + uri
337: + ", name=" + name + ", raw=" + raw
338: + ", attr=" + attr);
339: }
340:
341: // Element: insert
342: if (this .state == STATE_OUTSIDE
343: && (name.equals(INSERT_ELEMENT) || name
344: .equals(WRITE_ELEMENT))) {
345:
346: this .state = (name.equals(INSERT_ELEMENT) ? STATE_INSERT
347: : STATE_WRITE);
348: this .parent_state = this .state;
349: if (attr.getValue(CREATE_ATTRIBUTE) != null
350: && attr.getValue(CREATE_ATTRIBUTE).equals("false")) {
351: this .stack.push("false");
352: } else {
353: this .stack.push("true"); // default value
354: }
355: if (attr.getValue(OVERWRITE_ATTRIBUTE) != null
356: && attr.getValue(OVERWRITE_ATTRIBUTE).equals(
357: "false")) {
358: this .stack.push("false");
359: } else {
360: this .stack.push("true"); // default value
361: }
362: this .stack.push(attr.getValue(SERIALIZER_ATTRIBUTE));
363: this .stack.push("END");
364:
365: // Element: delete
366: } else if (this .state == STATE_OUTSIDE
367: && name.equals(DELETE_ELEMENT)) {
368: this .state = STATE_DELETE;
369: this .parent_state = state;
370: this .stack.push("END");
371: // Element: file
372: } else if (name.equals(SOURCE_ELEMENT)
373: && (this .state == STATE_INSERT
374: || this .state == STATE_WRITE || this .state == STATE_DELETE)) {
375: this .state = STATE_FILE;
376: this .startTextRecording();
377:
378: // Element: path
379: } else if (name.equals(PATH_ELEMENT)
380: && (this .state == STATE_INSERT
381: || this .state == STATE_WRITE || this .state == STATE_DELETE)) {
382: this .state = STATE_PATH;
383: this .startTextRecording();
384:
385: // Element: replace
386: } else if (name.equals(REPLACE_ELEMENT)
387: && this .state == STATE_INSERT) {
388: this .state = STATE_REPLACE;
389: this .startTextRecording();
390:
391: // Element: fragment
392: } else if (name.equals(FRAGMENT_ELEMENT)
393: && (this .state == STATE_INSERT
394: || this .state == STATE_WRITE || this .state == STATE_DELETE)) {
395: this .state = STATE_FRAGMENT;
396: this .startRecording();
397:
398: // Element: reinsert
399: } else if (name.equals(REINSERT_ELEMENT)
400: && this .state == STATE_INSERT) {
401: this .state = STATE_REINSERT;
402: this .startTextRecording();
403:
404: } else {
405: super .startTransformingElement(uri, name, raw, attr);
406: }
407: }
408:
409: /**
410: * Receive notification of the end of an element.
411: *
412: * @param uri The Namespace URI, or the empty string if the element has no
413: * Namespace URI or if Namespace
414: * processing is not being performed.
415: * @param name The local name (without prefix), or the empty string if
416: * Namespace processing is not being performed.
417: * @param raw The raw XML 1.0 name (with prefix), or the empty string if
418: * raw names are not available.
419: */
420: public void endTransformingElement(String uri, String name,
421: String raw) throws SAXException, IOException,
422: ProcessingException {
423: if (getLogger().isDebugEnabled()) {
424: getLogger().debug(
425: "End transforming element. uri=" + uri + ", name="
426: + name + ", raw=" + raw);
427: }
428:
429: if ((name.equals(INSERT_ELEMENT) && this .state == STATE_INSERT)
430: || (name.equals(WRITE_ELEMENT) && this .state == STATE_WRITE)) {
431:
432: // get the information from the stack
433: DocumentFragment fragment = null;
434: String tag;
435: String sourceName = null;
436: String path = (this .state == STATE_INSERT ? null : "/");
437: // source:write's path can be empty
438: String replacePath = null;
439: String reinsert = null;
440: do {
441: tag = (String) this .stack.pop();
442: if (tag.equals("PATH")) {
443: path = (String) this .stack.pop();
444: } else if (tag.equals("FILE")) {
445: sourceName = (String) this .stack.pop();
446: } else if (tag.equals("FRAGMENT")) {
447: fragment = (DocumentFragment) this .stack.pop();
448: } else if (tag.equals("REPLACE")) {
449: replacePath = (String) this .stack.pop();
450: } else if (tag.equals("REINSERT")) {
451: reinsert = (String) this .stack.pop();
452: }
453: } while (!tag.equals("END"));
454:
455: final String localSerializer = (String) this .stack.pop();
456: final boolean overwrite = this .stack.pop().equals("true");
457: final boolean create = this .stack.pop().equals("true");
458:
459: this .insertFragment(sourceName, path, fragment,
460: replacePath, create, overwrite, reinsert,
461: localSerializer, name);
462:
463: this .state = STATE_OUTSIDE;
464:
465: // Element: delete
466: } else if (name.equals(DELETE_ELEMENT)
467: && this .state == STATE_DELETE) {
468: String sourceName = null;
469: String tag;
470: do {
471: tag = (String) this .stack.pop();
472: if (tag.equals("FILE")) {
473: sourceName = (String) this .stack.pop();
474: } else if (tag.equals("FRAGMENT")) {
475: //Get rid of it
476: this .stack.pop();
477: }
478: } while (!tag.equals("END"));
479:
480: this .deleteSource(sourceName);
481: this .state = STATE_OUTSIDE;
482: // Element: file
483: } else if (name.equals(SOURCE_ELEMENT)
484: && this .state == STATE_FILE) {
485: this .state = this .parent_state;
486: this .stack.push(this .endTextRecording());
487: this .stack.push("FILE");
488:
489: // Element: path
490: } else if (name.equals(PATH_ELEMENT)
491: && this .state == STATE_PATH) {
492: this .state = this .parent_state;
493: this .stack.push(this .endTextRecording());
494: this .stack.push("PATH");
495:
496: // Element: replace
497: } else if (name.equals(REPLACE_ELEMENT)
498: && this .state == STATE_REPLACE) {
499: this .state = this .parent_state;
500: this .stack.push(this .endTextRecording());
501: this .stack.push("REPLACE");
502:
503: // Element: fragment
504: } else if (name.equals(FRAGMENT_ELEMENT)
505: && this .state == STATE_FRAGMENT) {
506: this .state = this .parent_state;
507: this .stack.push(this .endRecording());
508: this .stack.push("FRAGMENT");
509:
510: // Element: reinsert
511: } else if (name.equals(REINSERT_ELEMENT)
512: && this .state == STATE_REINSERT) {
513: this .state = this .parent_state;
514: this .stack.push(this .endTextRecording());
515: this .stack.push("REINSERT");
516:
517: // default
518: } else {
519: super .endTransformingElement(uri, name, raw);
520: }
521: }
522:
523: /**
524: * Deletes a source
525: * @param systemID
526: */
527: private void deleteSource(String systemID)
528: throws ProcessingException, IOException, SAXException {
529: Source source = null;
530: try {
531: source = resolver.resolveURI(systemID);
532: if (!(source instanceof ModifiableSource)) {
533: throw new ProcessingException("Source '" + systemID
534: + "' is not writeable.");
535: }
536:
537: ((ModifiableSource) source).delete();
538: reportResult("none", "delete",
539: "source deleted successfully", systemID,
540: RESULT_SUCCESS, ACTION_DELETE);
541: } catch (SourceException se) {
542: if (getLogger().isDebugEnabled()) {
543: getLogger().debug("FAIL exception: " + se, se);
544: }
545: reportResult("none", "delete", "unable to delete source: "
546: + se.getMessage(), systemID, RESULT_FAILED,
547: ACTION_DELETE);
548: } finally {
549: resolver.release(source);
550: }
551: }
552:
553: /**
554: * Insert a fragment into a file.
555: * The file is loaded by the resource connector.
556: *
557: * @param systemID The name of the xml file.
558: * @param path The XPath specifying the node under which the data is inserted
559: * @param fragment The data to be inserted.
560: * @param replacePath Optional XPath relative to <CODE>path</CODE>. This path
561: * can specify a node which will be removed if it exists.
562: * So insertFragment can be used as a replace utility.
563: * @param create If the file does not exists and this is set to
564: * <CODE>false</CODE> nothing is inserted. If it is set
565: * to <CODE>true</CODE> the file is created and the data
566: * is inserted.
567: * @param overwrite If this is set to <CODE>true</CODE> the data is only
568: * inserted if the node specified by the <CODE>replacePath</CODE>
569: * does not exists.
570: * @param reinsertPath If specified and a node is replaced , all children of
571: * this replaced node will be reinserted at the given path.
572: * @param localSerializer The serializer used to serialize the XML
573: * @param tagname The name of the tag that triggered me 'insert' or 'write'
574: */
575: protected void insertFragment(String systemID, String path,
576: DocumentFragment fragment, String replacePath,
577: boolean create, boolean overwrite, String reinsertPath,
578: String localSerializer, String tagname)
579: throws SAXException, IOException, ProcessingException {
580: // no sync req
581: if (getLogger().isDebugEnabled()) {
582: getLogger().debug(
583: "Insert fragment. systemID="
584: + systemID
585: + ", path="
586: + path
587: + ", replace="
588: + replacePath
589: + ", create="
590: + create
591: + ", overwrite="
592: + overwrite
593: + ", reinsert="
594: + reinsertPath
595: + ", fragment="
596: + (fragment == null ? "null" : XMLUtils
597: .serializeNode(fragment)));
598: }
599:
600: // test parameter
601: if (systemID == null) {
602: throw new ProcessingException(
603: "insertFragment: systemID is required.");
604: }
605: if (path == null) {
606: throw new ProcessingException(
607: "insertFragment: path is required.");
608: }
609: if (path.startsWith("/")) {
610: path = path.substring(1);
611: }
612: if (fragment == null) {
613: throw new ProcessingException(
614: "insertFragment: fragment is required.");
615: }
616:
617: // first: read the source as a DOM
618: Source source = null;
619: Document resource = null;
620: boolean failed = true;
621: boolean exists = false;
622: String message = "";
623: String target = systemID;
624: try {
625: source = this .resolver.resolveURI(systemID);
626: if (!(source instanceof ModifiableSource)) {
627: throw new ProcessingException("Source '" + systemID
628: + "' is not writeable.");
629: }
630: ModifiableSource ws = (ModifiableSource) source;
631: exists = ws.exists();
632: target = source.getURI();
633:
634: // Insert?
635: if (exists && this .state == STATE_INSERT) {
636: message = "content inserted at: " + path;
637: resource = SourceUtil.toDOM(source);
638: // import the fragment
639: Node importNode = resource.importNode(fragment, true);
640: // get the node
641: Node parent = DOMUtil.selectSingleNode(resource, path,
642: this .xpathProcessor);
643:
644: // replace?
645: if (replacePath != null) {
646: try {
647: Node replaceNode = DOMUtil.getSingleNode(
648: parent, replacePath,
649: this .xpathProcessor);
650: // now get the parent of this node until it is the parent node for insertion
651: while (replaceNode != null
652: && !replaceNode.getParentNode().equals(
653: parent)) {
654: replaceNode = replaceNode.getParentNode();
655: }
656:
657: if (replaceNode != null) {
658: if (overwrite) {
659: if (parent.getNodeType() == Node.DOCUMENT_NODE) {
660: // replacing of the document element is not allowed
661: resource = newDocument();
662: resource.appendChild(resource
663: .importNode(importNode,
664: true));
665: parent = resource;
666: replaceNode = resource.importNode(
667: replaceNode, true);
668: } else {
669: parent.replaceChild(importNode,
670: replaceNode);
671: }
672: message += ", replacing: "
673: + replacePath;
674: if (reinsertPath != null) {
675: Node insertAt = DOMUtil
676: .getSingleNode(parent,
677: reinsertPath,
678: this .xpathProcessor);
679: if (insertAt != null) {
680: while (replaceNode
681: .hasChildNodes()) {
682: insertAt
683: .appendChild(replaceNode
684: .getFirstChild());
685: }
686: } else { // reinsert point null
687: message = "replace failed, could not find your reinsert path: "
688: + reinsertPath;
689: resource = null;
690: }
691: }
692: } else { // overwrite was false
693: message = "replace failed, no overwrite allowed.";
694: resource = null;
695: }
696: } else { // specified replaceNode was not found
697: parent.appendChild(importNode);
698: }
699: } catch (javax.xml.transform.TransformerException sax) {
700: throw new ProcessingException(
701: "TransformerException: " + sax, sax);
702: }
703: } else { // no replace path, just do an insert at end
704: parent.appendChild(importNode);
705: }
706:
707: // Create?
708: } else if (create) {
709: // Create new document
710: resource = newDocument();
711:
712: // Import the fragment
713: Node importNode = resource.importNode(fragment, true);
714:
715: if (path.length() == 0) {
716: // Parent node is document itself
717: NodeList nodes = importNode.getChildNodes();
718: for (int i = 0; i < nodes.getLength();) {
719: Node node = nodes.item(i);
720: switch (node.getNodeType()) {
721: case Node.ELEMENT_NODE:
722: // May throw exception if fragment has more than one element
723: resource.appendChild(node);
724: break;
725:
726: case Node.DOCUMENT_TYPE_NODE:
727: case Node.PROCESSING_INSTRUCTION_NODE:
728: case Node.COMMENT_NODE:
729: resource.appendChild(node);
730: break;
731:
732: default:
733: // Ignore all other nodes
734: i++;
735: break;
736: }
737: }
738: message = "entire source overwritten";
739:
740: } else {
741: // Get the parent node
742: Node parent = DOMUtil.selectSingleNode(resource,
743: path, this .xpathProcessor);
744: // Add a fragment
745: parent.appendChild(importNode);
746: message = "content appended to: " + path;
747: }
748:
749: // Oops: Document does not exist and create is not allowed.
750: } else {
751: message = "create not allowed";
752: resource = null;/**/
753: }
754:
755: // Write source
756: if (resource != null) {
757: resource.normalize();
758: // use serializer
759: if (localSerializer == null) {
760: localSerializer = this .configuredSerializerName;
761: }
762:
763: if (localSerializer != null) {
764: // Lookup the Serializer
765: ServiceSelector selector = null;
766: Serializer serializer = null;
767: OutputStream oStream = null;
768: try {
769: selector = (ServiceSelector) manager
770: .lookup(Serializer.ROLE + "Selector");
771: serializer = (Serializer) selector
772: .select(localSerializer);
773: oStream = ws.getOutputStream();
774: serializer.setOutputStream(oStream);
775: DOMStreamer streamer = new DOMStreamer(
776: serializer);
777: streamer.stream(resource);
778: } finally {
779: if (oStream != null) {
780: oStream.flush();
781: try {
782: oStream.close();
783: failed = false;
784: } catch (Throwable t) {
785: if (getLogger().isDebugEnabled()) {
786: getLogger().debug(
787: "FAIL (oStream.close) exception"
788: + t, t);
789: }
790: throw new ProcessingException(
791: "Could not process your document.",
792: t);
793: } finally {
794: if (selector != null) {
795: selector.release(serializer);
796: this .manager.release(selector);
797: }
798: }
799: }
800: }
801: } else {
802: if (getLogger().isDebugEnabled()) {
803: getLogger().debug("ERROR: No serializer");
804: }
805: //throw new ProcessingException("No serializer specified for writing to source " + systemID);
806: message = "That source requires a serializer, please add the appropirate tag to your code.";
807: }
808: }
809: } catch (DOMException de) {
810: if (getLogger().isDebugEnabled()) {
811: getLogger().debug("FAIL exception: " + de, de);
812: }
813: message = "There was a problem manipulating your document: "
814: + de;
815: } catch (ServiceException ce) {
816: if (getLogger().isDebugEnabled()) {
817: getLogger().debug("FAIL exception: " + ce, ce);
818: }
819: message = "There was a problem looking up a component: "
820: + ce;
821: } catch (SourceException se) {
822: if (getLogger().isDebugEnabled()) {
823: getLogger().debug("FAIL exception: " + se, se);
824: }
825: message = "There was a problem resolving that source: ["
826: + systemID + "] : " + se;
827: } finally {
828: this .resolver.release(source);
829: }
830:
831: // Report result
832: String result = (failed) ? RESULT_FAILED : RESULT_SUCCESS;
833: String action = ACTION_NONE;
834: if (!failed) {
835: action = (exists) ? ACTION_OVER : ACTION_NEW;
836: }
837:
838: reportResult(localSerializer, tagname, message, target, result,
839: action);
840: }
841:
842: private void reportResult(String localSerializer, String tagname,
843: String message, String target, String result, String action)
844: throws SAXException {
845: sendStartElementEvent(RESULT_ELEMENT);
846: sendStartElementEvent(EXECUTION_ELEMENT);
847: sendTextEvent(result);
848: sendEndElementEvent(EXECUTION_ELEMENT);
849: sendStartElementEvent(MESSAGE_ELEMENT);
850: sendTextEvent(message);
851: sendEndElementEvent(MESSAGE_ELEMENT);
852: sendStartElementEvent(BEHAVIOUR_ELEMENT);
853: sendTextEvent(tagname);
854: sendEndElementEvent(BEHAVIOUR_ELEMENT);
855: sendStartElementEvent(ACTION_ELEMENT);
856: sendTextEvent(action);
857: sendEndElementEvent(ACTION_ELEMENT);
858: sendStartElementEvent(SOURCE_ELEMENT);
859: sendTextEvent(target);
860: sendEndElementEvent(SOURCE_ELEMENT);
861: if (localSerializer != null) {
862: sendStartElementEvent(SERIALIZER_ELEMENT);
863: sendTextEvent(localSerializer);
864: sendEndElementEvent(SERIALIZER_ELEMENT);
865: }
866: sendEndElementEvent(RESULT_ELEMENT);
867: }
868:
869: private Document newDocument() throws SAXException,
870: ServiceException {
871: DOMParser parser = (DOMParser) this .manager
872: .lookup(DOMParser.ROLE);
873: try {
874: return parser.createDocument();
875: } finally {
876: this .manager.release(parser);
877: }
878: }
879:
880: /* (non-Javadoc)
881: * @see org.apache.avalon.framework.service.Serviceable#service(ServiceManager)
882: */
883: public void service(ServiceManager manager) throws ServiceException {
884: super .service(manager);
885: this .xpathProcessor = (XPathProcessor) this .manager
886: .lookup(XPathProcessor.ROLE);
887: }
888:
889: /* (non-Javadoc)
890: * @see org.apache.avalon.framework.activity.Disposable#dispose()
891: */
892: public void dispose() {
893: if (this.manager != null) {
894: this.manager.release(this.xpathProcessor);
895: this.xpathProcessor = null;
896: }
897: super.dispose();
898: }
899: }
|