VirtualBox

source: vbox/trunk/src/VBox/Runtime/r3/xml.cpp@ 28689

Last change on this file since 28689 was 28689, checked in by vboxsync, 15 years ago

XmlFileWrite::write: Added a fSafe argument for safe writing of the xml file. See method description for details.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 41.1 KB
Line 
1/* $Id: xml.cpp 28689 2010-04-24 18:23:57Z vboxsync $ */
2/** @file
3 * IPRT - XML Manipulation API.
4 */
5
6/*
7 * Copyright (C) 2007-2010 Sun Microsystems, Inc.
8 *
9 * This file is part of VirtualBox Open Source Edition (OSE), as
10 * available from http://www.virtualbox.org. This file is free software;
11 * you can redistribute it and/or modify it under the terms of the GNU
12 * General Public License (GPL) as published by the Free Software
13 * Foundation, in version 2 as it comes in the "COPYING" file of the
14 * VirtualBox OSE distribution. VirtualBox OSE is distributed in the
15 * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
16 *
17 * The contents of this file may alternatively be used under the terms
18 * of the Common Development and Distribution License Version 1.0
19 * (CDDL) only, as it comes in the "COPYING.CDDL" file of the
20 * VirtualBox OSE distribution, in which case the provisions of the
21 * CDDL are applicable instead of those of the GPL.
22 *
23 * You may elect to license modified versions of this file under the
24 * terms and conditions of either the GPL or the CDDL or both.
25 *
26 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
27 * Clara, CA 95054 USA or visit http://www.sun.com if you need
28 * additional information or have any questions.
29 */
30
31#include <iprt/dir.h>
32#include <iprt/file.h>
33#include <iprt/err.h>
34#include <iprt/param.h>
35#include <iprt/path.h>
36#include <iprt/cpp/lock.h>
37#include <iprt/cpp/xml.h>
38
39#include <libxml/tree.h>
40#include <libxml/parser.h>
41#include <libxml/globals.h>
42#include <libxml/xmlIO.h>
43#include <libxml/xmlsave.h>
44#include <libxml/uri.h>
45
46#include <libxml/xmlschemas.h>
47
48#include <map>
49#include <boost/shared_ptr.hpp>
50
51////////////////////////////////////////////////////////////////////////////////
52//
53// globals
54//
55////////////////////////////////////////////////////////////////////////////////
56
57/**
58 * Global module initialization structure. This is to wrap non-reentrant bits
59 * of libxml, among other things.
60 *
61 * The constructor and destructor of this structure are used to perform global
62 * module initiaizaton and cleanup. There must be only one global variable of
63 * this structure.
64 */
65static
66class Global
67{
68public:
69
70 Global()
71 {
72 /* Check the parser version. The docs say it will kill the app if
73 * there is a serious version mismatch, but I couldn't find it in the
74 * source code (it only prints the error/warning message to the console) so
75 * let's leave it as is for informational purposes. */
76 LIBXML_TEST_VERSION
77
78 /* Init libxml */
79 xmlInitParser();
80
81 /* Save the default entity resolver before someone has replaced it */
82 sxml.defaultEntityLoader = xmlGetExternalEntityLoader();
83 }
84
85 ~Global()
86 {
87 /* Shutdown libxml */
88 xmlCleanupParser();
89 }
90
91 struct
92 {
93 xmlExternalEntityLoader defaultEntityLoader;
94
95 /** Used to provide some thread safety missing in libxml2 (see e.g.
96 * XmlTreeBackend::read()) */
97 RTLockMtx lock;
98 }
99 sxml; /* XXX naming this xml will break with gcc-3.3 */
100}
101gGlobal;
102
103
104
105namespace xml
106{
107
108////////////////////////////////////////////////////////////////////////////////
109//
110// Exceptions
111//
112////////////////////////////////////////////////////////////////////////////////
113
114LogicError::LogicError(RT_SRC_POS_DECL)
115 : Error(NULL)
116{
117 char *msg = NULL;
118 RTStrAPrintf(&msg, "In '%s', '%s' at #%d",
119 pszFunction, pszFile, iLine);
120 setWhat(msg);
121 RTStrFree(msg);
122}
123
124XmlError::XmlError(xmlErrorPtr aErr)
125{
126 if (!aErr)
127 throw EInvalidArg(RT_SRC_POS);
128
129 char *msg = Format(aErr);
130 setWhat(msg);
131 RTStrFree(msg);
132}
133
134/**
135 * Composes a single message for the given error. The caller must free the
136 * returned string using RTStrFree() when no more necessary.
137 */
138// static
139char *XmlError::Format(xmlErrorPtr aErr)
140{
141 const char *msg = aErr->message ? aErr->message : "<none>";
142 size_t msgLen = strlen(msg);
143 /* strip spaces, trailing EOLs and dot-like char */
144 while (msgLen && strchr(" \n.?!", msg [msgLen - 1]))
145 --msgLen;
146
147 char *finalMsg = NULL;
148 RTStrAPrintf(&finalMsg, "%.*s.\nLocation: '%s', line %d (%d), column %d",
149 msgLen, msg, aErr->file, aErr->line, aErr->int1, aErr->int2);
150
151 return finalMsg;
152}
153
154EIPRTFailure::EIPRTFailure(int aRC, const char *pcszContext, ...)
155 : RuntimeError(NULL),
156 mRC(aRC)
157{
158 char *pszContext2;
159 va_list args;
160 va_start(args, pcszContext);
161 RTStrAPrintfV(&pszContext2, pcszContext, args);
162 char *newMsg;
163 RTStrAPrintf(&newMsg, "%s: %d (%s)", pszContext2, aRC, RTErrGetShort(aRC));
164 setWhat(newMsg);
165 RTStrFree(newMsg);
166 RTStrFree(pszContext2);
167}
168
169////////////////////////////////////////////////////////////////////////////////
170//
171// File Class
172//
173//////////////////////////////////////////////////////////////////////////////
174
175struct File::Data
176{
177 Data()
178 : handle(NIL_RTFILE), opened(false)
179 { }
180
181 iprt::MiniString strFileName;
182 RTFILE handle;
183 bool opened : 1;
184 bool flushOnClose : 1;
185};
186
187File::File(Mode aMode, const char *aFileName, bool aFlushIt /* = false */)
188 : m(new Data())
189{
190 m->strFileName = aFileName;
191 m->flushOnClose = aFlushIt;
192
193 uint32_t flags = 0;
194 switch (aMode)
195 {
196 /** @todo change to RTFILE_O_DENY_WRITE where appropriate. */
197 case Mode_Read:
198 flags = RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_NONE;
199 break;
200 case Mode_WriteCreate: // fail if file exists
201 flags = RTFILE_O_WRITE | RTFILE_O_CREATE | RTFILE_O_DENY_NONE;
202 break;
203 case Mode_Overwrite: // overwrite if file exists
204 flags = RTFILE_O_WRITE | RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE;
205 break;
206 case Mode_ReadWrite:
207 flags = RTFILE_O_READWRITE | RTFILE_O_OPEN | RTFILE_O_DENY_NONE;;
208 }
209
210 int vrc = RTFileOpen(&m->handle, aFileName, flags);
211 if (RT_FAILURE(vrc))
212 throw EIPRTFailure(vrc, "Runtime error opening '%s' for reading", aFileName);
213
214 m->opened = true;
215 m->flushOnClose = aFlushIt && (flags & RTFILE_O_ACCESS_MASK) != RTFILE_O_READ;
216}
217
218File::File(RTFILE aHandle, const char *aFileName /* = NULL */, bool aFlushIt /* = false */)
219 : m(new Data())
220{
221 if (aHandle == NIL_RTFILE)
222 throw EInvalidArg(RT_SRC_POS);
223
224 m->handle = aHandle;
225
226 if (aFileName)
227 m->strFileName = aFileName;
228
229 m->flushOnClose = aFlushIt;
230
231 setPos(0);
232}
233
234File::~File()
235{
236 if (m->flushOnClose)
237 {
238 RTFileFlush(m->handle);
239 if (!m->strFileName.isEmpty())
240 RTDirFlushParent(m->strFileName.c_str());
241 }
242
243 if (m->opened)
244 RTFileClose(m->handle);
245 delete m;
246}
247
248const char* File::uri() const
249{
250 return m->strFileName.c_str();
251}
252
253uint64_t File::pos() const
254{
255 uint64_t p = 0;
256 int vrc = RTFileSeek(m->handle, 0, RTFILE_SEEK_CURRENT, &p);
257 if (RT_SUCCESS(vrc))
258 return p;
259
260 throw EIPRTFailure(vrc, "Runtime error seeking in file '%s'", m->strFileName.c_str());
261}
262
263void File::setPos(uint64_t aPos)
264{
265 uint64_t p = 0;
266 unsigned method = RTFILE_SEEK_BEGIN;
267 int vrc = VINF_SUCCESS;
268
269 /* check if we overflow int64_t and move to INT64_MAX first */
270 if ((int64_t)aPos < 0)
271 {
272 vrc = RTFileSeek(m->handle, INT64_MAX, method, &p);
273 aPos -= (uint64_t)INT64_MAX;
274 method = RTFILE_SEEK_CURRENT;
275 }
276 /* seek the rest */
277 if (RT_SUCCESS(vrc))
278 vrc = RTFileSeek(m->handle, (int64_t) aPos, method, &p);
279 if (RT_SUCCESS(vrc))
280 return;
281
282 throw EIPRTFailure(vrc, "Runtime error seeking in file '%s'", m->strFileName.c_str());
283}
284
285int File::read(char *aBuf, int aLen)
286{
287 size_t len = aLen;
288 int vrc = RTFileRead(m->handle, aBuf, len, &len);
289 if (RT_SUCCESS(vrc))
290 return (int)len;
291
292 throw EIPRTFailure(vrc, "Runtime error reading from file '%s'", m->strFileName.c_str());
293}
294
295int File::write(const char *aBuf, int aLen)
296{
297 size_t len = aLen;
298 int vrc = RTFileWrite (m->handle, aBuf, len, &len);
299 if (RT_SUCCESS (vrc))
300 return (int)len;
301
302 throw EIPRTFailure(vrc, "Runtime error writing to file '%s'", m->strFileName.c_str());
303
304 return -1 /* failure */;
305}
306
307void File::truncate()
308{
309 int vrc = RTFileSetSize (m->handle, pos());
310 if (RT_SUCCESS (vrc))
311 return;
312
313 throw EIPRTFailure(vrc, "Runtime error truncating file '%s'", m->strFileName.c_str());
314}
315
316////////////////////////////////////////////////////////////////////////////////
317//
318// MemoryBuf Class
319//
320//////////////////////////////////////////////////////////////////////////////
321
322struct MemoryBuf::Data
323{
324 Data()
325 : buf (NULL), len (0), uri (NULL), pos (0) {}
326
327 const char *buf;
328 size_t len;
329 char *uri;
330
331 size_t pos;
332};
333
334MemoryBuf::MemoryBuf (const char *aBuf, size_t aLen, const char *aURI /* = NULL */)
335 : m (new Data())
336{
337 if (aBuf == NULL)
338 throw EInvalidArg (RT_SRC_POS);
339
340 m->buf = aBuf;
341 m->len = aLen;
342 m->uri = RTStrDup (aURI);
343}
344
345MemoryBuf::~MemoryBuf()
346{
347 RTStrFree (m->uri);
348}
349
350const char *MemoryBuf::uri() const
351{
352 return m->uri;
353}
354
355uint64_t MemoryBuf::pos() const
356{
357 return m->pos;
358}
359
360void MemoryBuf::setPos (uint64_t aPos)
361{
362 size_t off = (size_t) aPos;
363 if ((uint64_t) off != aPos)
364 throw EInvalidArg();
365
366 if (off > m->len)
367 throw EInvalidArg();
368
369 m->pos = off;
370}
371
372int MemoryBuf::read (char *aBuf, int aLen)
373{
374 if (m->pos >= m->len)
375 return 0 /* nothing to read */;
376
377 size_t len = m->pos + aLen < m->len ? aLen : m->len - m->pos;
378 memcpy (aBuf, m->buf + m->pos, len);
379 m->pos += len;
380
381 return (int)len;
382}
383
384////////////////////////////////////////////////////////////////////////////////
385//
386// GlobalLock class
387//
388////////////////////////////////////////////////////////////////////////////////
389
390struct GlobalLock::Data
391{
392 PFNEXTERNALENTITYLOADER pOldLoader;
393 RTLock lock;
394
395 Data()
396 : pOldLoader(NULL),
397 lock(gGlobal.sxml.lock)
398 {
399 }
400};
401
402GlobalLock::GlobalLock()
403 : m(new Data())
404{
405}
406
407GlobalLock::~GlobalLock()
408{
409 if (m->pOldLoader)
410 xmlSetExternalEntityLoader(m->pOldLoader);
411 delete m;
412 m = NULL;
413}
414
415void GlobalLock::setExternalEntityLoader(PFNEXTERNALENTITYLOADER pLoader)
416{
417 m->pOldLoader = xmlGetExternalEntityLoader();
418 xmlSetExternalEntityLoader(pLoader);
419}
420
421// static
422xmlParserInput* GlobalLock::callDefaultLoader(const char *aURI,
423 const char *aID,
424 xmlParserCtxt *aCtxt)
425{
426 return gGlobal.sxml.defaultEntityLoader(aURI, aID, aCtxt);
427}
428
429////////////////////////////////////////////////////////////////////////////////
430//
431// Node class
432//
433////////////////////////////////////////////////////////////////////////////////
434
435struct Node::Data
436{
437 struct compare_const_char
438 {
439 bool operator()(const char* s1, const char* s2) const
440 {
441 return strcmp(s1, s2) < 0;
442 }
443 };
444
445 // attributes, if this is an element; can be empty
446 typedef std::map<const char*, boost::shared_ptr<AttributeNode>, compare_const_char > AttributesMap;
447 AttributesMap attribs;
448
449 // child elements, if this is an element; can be empty
450 typedef std::list< boost::shared_ptr<Node> > InternalNodesList;
451 InternalNodesList children;
452};
453
454Node::Node(EnumType type,
455 Node *pParent,
456 xmlNode *plibNode,
457 xmlAttr *plibAttr)
458 : m_Type(type),
459 m_pParent(pParent),
460 m_plibNode(plibNode),
461 m_plibAttr(plibAttr),
462 m_pcszNamespacePrefix(NULL),
463 m_pcszNamespaceHref(NULL),
464 m_pcszName(NULL),
465 m(new Data)
466{
467}
468
469Node::~Node()
470{
471 delete m;
472}
473
474void Node::buildChildren(const ElementNode &elmRoot) // private
475{
476 // go thru this element's attributes
477 xmlAttr *plibAttr = m_plibNode->properties;
478 while (plibAttr)
479 {
480 const char *pcszKey;
481 boost::shared_ptr<AttributeNode> pNew(new AttributeNode(elmRoot, this, plibAttr, &pcszKey));
482 // store
483 m->attribs[pcszKey] = pNew;
484
485 plibAttr = plibAttr->next;
486 }
487
488 // go thru this element's child elements
489 xmlNodePtr plibNode = m_plibNode->children;
490 while (plibNode)
491 {
492 boost::shared_ptr<Node> pNew;
493
494 if (plibNode->type == XML_ELEMENT_NODE)
495 pNew = boost::shared_ptr<Node>(new ElementNode(&elmRoot, this, plibNode));
496 else if (plibNode->type == XML_TEXT_NODE)
497 pNew = boost::shared_ptr<Node>(new ContentNode(this, plibNode));
498 if (pNew)
499 {
500 // store
501 m->children.push_back(pNew);
502
503 // recurse for this child element to get its own children
504 pNew->buildChildren(elmRoot);
505 }
506
507 plibNode = plibNode->next;
508 }
509}
510
511/**
512 * Returns the name of the node, which is either the element name or
513 * the attribute name. For other node types it probably returns NULL.
514 * @return
515 */
516const char* Node::getName() const
517{
518 return m_pcszName;
519}
520
521/**
522 * Variant of nameEquals that checks the namespace as well.
523 * @param pcszNamespace
524 * @param pcsz
525 * @return
526 */
527bool Node::nameEquals(const char *pcszNamespace, const char *pcsz) const
528{
529 if (m_pcszName == pcsz)
530 return true;
531 if (m_pcszName == NULL)
532 return false;
533 if (pcsz == NULL)
534 return false;
535 if (strcmp(m_pcszName, pcsz))
536 return false;
537
538 // name matches: then check namespaces as well
539 if (!pcszNamespace)
540 return true;
541 // caller wants namespace:
542 if (!m_pcszNamespacePrefix)
543 // but node has no namespace:
544 return false;
545 return !strcmp(m_pcszNamespacePrefix, pcszNamespace);
546}
547
548/**
549 * Returns the value of a node. If this node is an attribute, returns
550 * the attribute value; if this node is an element, then this returns
551 * the element text content.
552 * @return
553 */
554const char* Node::getValue() const
555{
556 if ( (m_plibAttr)
557 && (m_plibAttr->children)
558 )
559 // libxml hides attribute values in another node created as a
560 // single child of the attribute node, and it's in the content field
561 return (const char*)m_plibAttr->children->content;
562
563 if ( (m_plibNode)
564 && (m_plibNode->children)
565 )
566 return (const char*)m_plibNode->children->content;
567
568 return NULL;
569}
570
571/**
572 * Copies the value of a node into the given integer variable.
573 * Returns TRUE only if a value was found and was actually an
574 * integer of the given type.
575 * @return
576 */
577bool Node::copyValue(int32_t &i) const
578{
579 const char *pcsz;
580 if ( ((pcsz = getValue()))
581 && (VINF_SUCCESS == RTStrToInt32Ex(pcsz, NULL, 10, &i))
582 )
583 return true;
584
585 return false;
586}
587
588/**
589 * Copies the value of a node into the given integer variable.
590 * Returns TRUE only if a value was found and was actually an
591 * integer of the given type.
592 * @return
593 */
594bool Node::copyValue(uint32_t &i) const
595{
596 const char *pcsz;
597 if ( ((pcsz = getValue()))
598 && (VINF_SUCCESS == RTStrToUInt32Ex(pcsz, NULL, 10, &i))
599 )
600 return true;
601
602 return false;
603}
604
605/**
606 * Copies the value of a node into the given integer variable.
607 * Returns TRUE only if a value was found and was actually an
608 * integer of the given type.
609 * @return
610 */
611bool Node::copyValue(int64_t &i) const
612{
613 const char *pcsz;
614 if ( ((pcsz = getValue()))
615 && (VINF_SUCCESS == RTStrToInt64Ex(pcsz, NULL, 10, &i))
616 )
617 return true;
618
619 return false;
620}
621
622/**
623 * Copies the value of a node into the given integer variable.
624 * Returns TRUE only if a value was found and was actually an
625 * integer of the given type.
626 * @return
627 */
628bool Node::copyValue(uint64_t &i) const
629{
630 const char *pcsz;
631 if ( ((pcsz = getValue()))
632 && (VINF_SUCCESS == RTStrToUInt64Ex(pcsz, NULL, 10, &i))
633 )
634 return true;
635
636 return false;
637}
638
639/**
640 * Returns the line number of the current node in the source XML file.
641 * Useful for error messages.
642 * @return
643 */
644int Node::getLineNumber() const
645{
646 if (m_plibAttr)
647 return m_pParent->m_plibNode->line;
648
649 return m_plibNode->line;
650}
651
652ElementNode::ElementNode(const ElementNode *pelmRoot,
653 Node *pParent,
654 xmlNode *plibNode)
655 : Node(IsElement,
656 pParent,
657 plibNode,
658 NULL)
659{
660 if (!(m_pelmRoot = pelmRoot))
661 // NULL passed, then this is the root element
662 m_pelmRoot = this;
663
664 m_pcszName = (const char*)plibNode->name;
665
666 if (plibNode->ns)
667 {
668 m_pcszNamespacePrefix = (const char*)m_plibNode->ns->prefix;
669 m_pcszNamespaceHref = (const char*)m_plibNode->ns->href;
670 }
671}
672
673/**
674 * Builds a list of direct child elements of the current element that
675 * match the given string; if pcszMatch is NULL, all direct child
676 * elements are returned.
677 * @param children out: list of nodes to which children will be appended.
678 * @param pcszMatch in: match string, or NULL to return all children.
679 * @return Number of items appended to the list (0 if none).
680 */
681int ElementNode::getChildElements(ElementNodesList &children,
682 const char *pcszMatch /*= NULL*/)
683 const
684{
685 int i = 0;
686 for (Data::InternalNodesList::iterator it = m->children.begin();
687 it != m->children.end();
688 ++it)
689 {
690 // export this child node if ...
691 Node *p = it->get();
692 if (p->isElement())
693 if ( (!pcszMatch) // the caller wants all nodes or
694 || (!strcmp(pcszMatch, p->getName())) // the element name matches
695 )
696 {
697 children.push_back(static_cast<ElementNode*>(p));
698 ++i;
699 }
700 }
701 return i;
702}
703
704/**
705 * Returns the first child element whose name matches pcszMatch.
706 *
707 * @param pcszNamespace Namespace prefix (e.g. "vbox") or NULL to match any namespace.
708 * @param pcszMatch Element name to match.
709 * @return
710 */
711const ElementNode* ElementNode::findChildElement(const char *pcszNamespace,
712 const char *pcszMatch)
713 const
714{
715 Data::InternalNodesList::const_iterator
716 it,
717 last = m->children.end();
718 for (it = m->children.begin();
719 it != last;
720 ++it)
721 {
722 if ((**it).isElement())
723 {
724 ElementNode *pelm = static_cast<ElementNode*>((*it).get());
725 if (pelm->nameEquals(pcszNamespace, pcszMatch))
726 return pelm;
727 }
728 }
729
730 return NULL;
731}
732
733/**
734 * Returns the first child element whose "id" attribute matches pcszId.
735 * @param pcszId identifier to look for.
736 * @return child element or NULL if not found.
737 */
738const ElementNode* ElementNode::findChildElementFromId(const char *pcszId) const
739{
740 Data::InternalNodesList::const_iterator
741 it,
742 last = m->children.end();
743 for (it = m->children.begin();
744 it != last;
745 ++it)
746 {
747 if ((**it).isElement())
748 {
749 ElementNode *pelm = static_cast<ElementNode*>((*it).get());
750 const AttributeNode *pAttr;
751 if ( ((pAttr = pelm->findAttribute("id")))
752 && (!strcmp(pAttr->getValue(), pcszId))
753 )
754 return pelm;
755 }
756 }
757
758 return NULL;
759}
760
761/**
762 * Looks up the given attribute node in this element's attribute map.
763 *
764 * With respect to namespaces, the internal attributes map stores namespace
765 * prefixes with attribute names only if the attribute uses a non-default
766 * namespace. As a result, the following rules apply:
767 *
768 * -- To find attributes from a non-default namespace, pcszMatch must not
769 * be prefixed with a namespace.
770 *
771 * -- To find attributes from the default namespace (or if the document does
772 * not use namespaces), pcszMatch must be prefixed with the namespace
773 * prefix and a colon.
774 *
775 * For example, if the document uses the "vbox:" namespace by default, you
776 * must omit "vbox:" from pcszMatch to find such attributes, whether they
777 * are specifed in the xml or not.
778 *
779 * @param pcszMatch
780 * @return
781 */
782const AttributeNode* ElementNode::findAttribute(const char *pcszMatch) const
783{
784 Data::AttributesMap::const_iterator it;
785
786 it = m->attribs.find(pcszMatch);
787 if (it != m->attribs.end())
788 return it->second.get();
789
790 return NULL;
791}
792
793/**
794 * Convenience method which attempts to find the attribute with the given
795 * name and returns its value as a string.
796 *
797 * @param pcszMatch name of attribute to find (see findAttribute() for namespace remarks)
798 * @param ppcsz out: attribute value
799 * @return TRUE if attribute was found and str was thus updated.
800 */
801bool ElementNode::getAttributeValue(const char *pcszMatch, const char *&ppcsz) const
802{
803 const Node* pAttr;
804 if ((pAttr = findAttribute(pcszMatch)))
805 {
806 ppcsz = pAttr->getValue();
807 return true;
808 }
809
810 return false;
811}
812
813/**
814 * Convenience method which attempts to find the attribute with the given
815 * name and returns its value as a string.
816 *
817 * @param pcszMatch name of attribute to find (see findAttribute() for namespace remarks)
818 * @param str out: attribute value; overwritten only if attribute was found
819 * @return TRUE if attribute was found and str was thus updated.
820 */
821bool ElementNode::getAttributeValue(const char *pcszMatch, iprt::MiniString &str) const
822{
823 const Node* pAttr;
824 if ((pAttr = findAttribute(pcszMatch)))
825 {
826 str = pAttr->getValue();
827 return true;
828 }
829
830 return false;
831}
832
833/**
834 * Convenience method which attempts to find the attribute with the given
835 * name and returns its value as a signed integer. This calls
836 * RTStrToInt32Ex internally and will only output the integer if that
837 * function returns no error.
838 *
839 * @param pcszMatch name of attribute to find (see findAttribute() for namespace remarks)
840 * @param i out: attribute value; overwritten only if attribute was found
841 * @return TRUE if attribute was found and str was thus updated.
842 */
843bool ElementNode::getAttributeValue(const char *pcszMatch, int32_t &i) const
844{
845 const char *pcsz;
846 if ( (getAttributeValue(pcszMatch, pcsz))
847 && (VINF_SUCCESS == RTStrToInt32Ex(pcsz, NULL, 0, &i))
848 )
849 return true;
850
851 return false;
852}
853
854/**
855 * Convenience method which attempts to find the attribute with the given
856 * name and returns its value as an unsigned integer.This calls
857 * RTStrToUInt32Ex internally and will only output the integer if that
858 * function returns no error.
859 *
860 * @param pcszMatch name of attribute to find (see findAttribute() for namespace remarks)
861 * @param i out: attribute value; overwritten only if attribute was found
862 * @return TRUE if attribute was found and str was thus updated.
863 */
864bool ElementNode::getAttributeValue(const char *pcszMatch, uint32_t &i) const
865{
866 const char *pcsz;
867 if ( (getAttributeValue(pcszMatch, pcsz))
868 && (VINF_SUCCESS == RTStrToUInt32Ex(pcsz, NULL, 0, &i))
869 )
870 return true;
871
872 return false;
873}
874
875/**
876 * Convenience method which attempts to find the attribute with the given
877 * name and returns its value as a signed long integer. This calls
878 * RTStrToInt64Ex internally and will only output the integer if that
879 * function returns no error.
880 *
881 * @param pcszMatch name of attribute to find (see findAttribute() for namespace remarks)
882 * @param i out: attribute value
883 * @return TRUE if attribute was found and str was thus updated.
884 */
885bool ElementNode::getAttributeValue(const char *pcszMatch, int64_t &i) const
886{
887 const char *pcsz;
888 if ( (getAttributeValue(pcszMatch, pcsz))
889 && (VINF_SUCCESS == RTStrToInt64Ex(pcsz, NULL, 0, &i))
890 )
891 return true;
892
893 return false;
894}
895
896/**
897 * Convenience method which attempts to find the attribute with the given
898 * name and returns its value as an unsigned long integer.This calls
899 * RTStrToUInt64Ex internally and will only output the integer if that
900 * function returns no error.
901 *
902 * @param pcszMatch name of attribute to find (see findAttribute() for namespace remarks)
903 * @param i out: attribute value; overwritten only if attribute was found
904 * @return TRUE if attribute was found and str was thus updated.
905 */
906bool ElementNode::getAttributeValue(const char *pcszMatch, uint64_t &i) const
907{
908 const char *pcsz;
909 if ( (getAttributeValue(pcszMatch, pcsz))
910 && (VINF_SUCCESS == RTStrToUInt64Ex(pcsz, NULL, 0, &i))
911 )
912 return true;
913
914 return false;
915}
916
917/**
918 * Convenience method which attempts to find the attribute with the given
919 * name and returns its value as a boolean. This accepts "true", "false",
920 * "yes", "no", "1" or "0" as valid values.
921 *
922 * @param pcszMatch name of attribute to find (see findAttribute() for namespace remarks)
923 * @param f out: attribute value; overwritten only if attribute was found
924 * @return TRUE if attribute was found and str was thus updated.
925 */
926bool ElementNode::getAttributeValue(const char *pcszMatch, bool &f) const
927{
928 const char *pcsz;
929 if (getAttributeValue(pcszMatch, pcsz))
930 {
931 if ( (!strcmp(pcsz, "true"))
932 || (!strcmp(pcsz, "yes"))
933 || (!strcmp(pcsz, "1"))
934 )
935 {
936 f = true;
937 return true;
938 }
939 if ( (!strcmp(pcsz, "false"))
940 || (!strcmp(pcsz, "no"))
941 || (!strcmp(pcsz, "0"))
942 )
943 {
944 f = false;
945 return true;
946 }
947 }
948
949 return false;
950}
951
952/**
953 * Creates a new child element node and appends it to the list
954 * of children in "this".
955 *
956 * @param pcszElementName
957 * @return
958 */
959ElementNode* ElementNode::createChild(const char *pcszElementName)
960{
961 // we must be an element, not an attribute
962 if (!m_plibNode)
963 throw ENodeIsNotElement(RT_SRC_POS);
964
965 // libxml side: create new node
966 xmlNode *plibNode;
967 if (!(plibNode = xmlNewNode(NULL, // namespace
968 (const xmlChar*)pcszElementName)))
969 throw std::bad_alloc();
970 xmlAddChild(m_plibNode, plibNode);
971
972 // now wrap this in C++
973 ElementNode *p = new ElementNode(m_pelmRoot, this, plibNode);
974 boost::shared_ptr<ElementNode> pNew(p);
975 m->children.push_back(pNew);
976
977 return p;
978}
979
980
981/**
982 * Creates a content node and appends it to the list of children
983 * in "this".
984 *
985 * @param pcszContent
986 * @return
987 */
988ContentNode* ElementNode::addContent(const char *pcszContent)
989{
990 // libxml side: create new node
991 xmlNode *plibNode;
992 if (!(plibNode = xmlNewText((const xmlChar*)pcszContent)))
993 throw std::bad_alloc();
994 xmlAddChild(m_plibNode, plibNode);
995
996 // now wrap this in C++
997 ContentNode *p = new ContentNode(this, plibNode);
998 boost::shared_ptr<ContentNode> pNew(p);
999 m->children.push_back(pNew);
1000
1001 return p;
1002}
1003
1004/**
1005 * Sets the given attribute.
1006 *
1007 * If an attribute with the given name exists, it is overwritten,
1008 * otherwise a new attribute is created. Returns the attribute node
1009 * that was either created or changed.
1010 *
1011 * @param pcszName
1012 * @param pcszValue
1013 * @return
1014 */
1015AttributeNode* ElementNode::setAttribute(const char *pcszName, const char *pcszValue)
1016{
1017 Data::AttributesMap::const_iterator it;
1018
1019 it = m->attribs.find(pcszName);
1020 if (it == m->attribs.end())
1021 {
1022 // libxml side: xmlNewProp creates an attribute
1023 xmlAttr *plibAttr = xmlNewProp(m_plibNode, (xmlChar*)pcszName, (xmlChar*)pcszValue);
1024
1025 // C++ side: create an attribute node around it
1026 const char *pcszKey;
1027 boost::shared_ptr<AttributeNode> pNew(new AttributeNode(*m_pelmRoot, this, plibAttr, &pcszKey));
1028 // store
1029 m->attribs[pcszKey] = pNew;
1030 }
1031 else
1032 {
1033 // @todo
1034 throw LogicError("Attribute exists");
1035 }
1036
1037 return NULL;
1038
1039}
1040
1041AttributeNode* ElementNode::setAttribute(const char *pcszName, int32_t i)
1042{
1043 char *psz = NULL;
1044 RTStrAPrintf(&psz, "%RI32", i);
1045 AttributeNode *p = setAttribute(pcszName, psz);
1046 RTStrFree(psz);
1047 return p;
1048}
1049
1050AttributeNode* ElementNode::setAttribute(const char *pcszName, uint32_t i)
1051{
1052 char *psz = NULL;
1053 RTStrAPrintf(&psz, "%RU32", i);
1054 AttributeNode *p = setAttribute(pcszName, psz);
1055 RTStrFree(psz);
1056 return p;
1057}
1058
1059AttributeNode* ElementNode::setAttribute(const char *pcszName, int64_t i)
1060{
1061 char *psz = NULL;
1062 RTStrAPrintf(&psz, "%RI64", i);
1063 AttributeNode *p = setAttribute(pcszName, psz);
1064 RTStrFree(psz);
1065 return p;
1066}
1067
1068AttributeNode* ElementNode::setAttribute(const char *pcszName, uint64_t i)
1069{
1070 char *psz = NULL;
1071 RTStrAPrintf(&psz, "%RU64", i);
1072 AttributeNode *p = setAttribute(pcszName, psz);
1073 RTStrFree(psz);
1074 return p;
1075}
1076
1077AttributeNode* ElementNode::setAttributeHex(const char *pcszName, uint32_t i)
1078{
1079 char *psz = NULL;
1080 RTStrAPrintf(&psz, "0x%RX32", i);
1081 AttributeNode *p = setAttribute(pcszName, psz);
1082 RTStrFree(psz);
1083 return p;
1084}
1085
1086AttributeNode* ElementNode::setAttribute(const char *pcszName, bool f)
1087{
1088 return setAttribute(pcszName, (f) ? "true" : "false");
1089}
1090
1091/**
1092 * Private constructur for a new attribute node. This one is special:
1093 * in ppcszKey, it returns a pointer to a string buffer that should be
1094 * used to index the attribute correctly with namespaces.
1095 *
1096 * @param pParent
1097 * @param elmRoot
1098 * @param plibAttr
1099 * @param ppcszKey
1100 */
1101AttributeNode::AttributeNode(const ElementNode &elmRoot,
1102 Node *pParent,
1103 xmlAttr *plibAttr,
1104 const char **ppcszKey)
1105 : Node(IsAttribute,
1106 pParent,
1107 NULL,
1108 plibAttr)
1109{
1110 m_pcszName = (const char*)plibAttr->name;
1111
1112 *ppcszKey = m_pcszName;
1113
1114 if ( plibAttr->ns
1115 && plibAttr->ns->prefix
1116 )
1117 {
1118 m_pcszNamespacePrefix = (const char*)plibAttr->ns->prefix;
1119 m_pcszNamespaceHref = (const char*)plibAttr->ns->href;
1120
1121 if ( !elmRoot.m_pcszNamespaceHref
1122 || (strcmp(m_pcszNamespaceHref, elmRoot.m_pcszNamespaceHref))
1123 )
1124 {
1125 // not default namespace:
1126 m_strKey = m_pcszNamespacePrefix;
1127 m_strKey.append(':');
1128 m_strKey.append(m_pcszName);
1129
1130 *ppcszKey = m_strKey.c_str();
1131 }
1132 }
1133}
1134
1135ContentNode::ContentNode(Node *pParent, xmlNode *plibNode)
1136 : Node(IsContent,
1137 pParent,
1138 plibNode,
1139 NULL)
1140{
1141}
1142
1143/*
1144 * NodesLoop
1145 *
1146 */
1147
1148struct NodesLoop::Data
1149{
1150 ElementNodesList listElements;
1151 ElementNodesList::const_iterator it;
1152};
1153
1154NodesLoop::NodesLoop(const ElementNode &node, const char *pcszMatch /* = NULL */)
1155{
1156 m = new Data;
1157 node.getChildElements(m->listElements, pcszMatch);
1158 m->it = m->listElements.begin();
1159}
1160
1161NodesLoop::~NodesLoop()
1162{
1163 delete m;
1164}
1165
1166
1167/**
1168 * Handy convenience helper for looping over all child elements. Create an
1169 * instance of NodesLoop on the stack and call this method until it returns
1170 * NULL, like this:
1171 * <code>
1172 * xml::ElementNode node; // should point to an element
1173 * xml::NodesLoop loop(node, "child"); // find all "child" elements under node
1174 * const xml::ElementNode *pChild = NULL;
1175 * while (pChild = loop.forAllNodes())
1176 * ...;
1177 * </code>
1178 * @return
1179 */
1180const ElementNode* NodesLoop::forAllNodes() const
1181{
1182 const ElementNode *pNode = NULL;
1183
1184 if (m->it != m->listElements.end())
1185 {
1186 pNode = *(m->it);
1187 ++(m->it);
1188 }
1189
1190 return pNode;
1191}
1192
1193////////////////////////////////////////////////////////////////////////////////
1194//
1195// Document class
1196//
1197////////////////////////////////////////////////////////////////////////////////
1198
1199struct Document::Data
1200{
1201 xmlDocPtr plibDocument;
1202 ElementNode *pRootElement;
1203
1204 Data()
1205 {
1206 plibDocument = NULL;
1207 pRootElement = NULL;
1208 }
1209
1210 ~Data()
1211 {
1212 reset();
1213 }
1214
1215 void reset()
1216 {
1217 if (plibDocument)
1218 {
1219 xmlFreeDoc(plibDocument);
1220 plibDocument = NULL;
1221 }
1222 if (pRootElement)
1223 {
1224 delete pRootElement;
1225 pRootElement = NULL;
1226 }
1227 }
1228
1229 void copyFrom(const Document::Data *p)
1230 {
1231 if (p->plibDocument)
1232 {
1233 plibDocument = xmlCopyDoc(p->plibDocument,
1234 1); // recursive == copy all
1235 }
1236 }
1237};
1238
1239Document::Document()
1240 : m(new Data)
1241{
1242}
1243
1244Document::Document(const Document &x)
1245 : m(new Data)
1246{
1247 m->copyFrom(x.m);
1248}
1249
1250Document& Document::operator=(const Document &x)
1251{
1252 m->reset();
1253 m->copyFrom(x.m);
1254 return *this;
1255}
1256
1257Document::~Document()
1258{
1259 delete m;
1260}
1261
1262/**
1263 * private method to refresh all internal structures after the internal pDocument
1264 * has changed. Called from XmlFileParser::read(). m->reset() must have been
1265 * called before to make sure all members except the internal pDocument are clean.
1266 */
1267void Document::refreshInternals() // private
1268{
1269 m->pRootElement = new ElementNode(NULL, NULL, xmlDocGetRootElement(m->plibDocument));
1270
1271 m->pRootElement->buildChildren(*m->pRootElement);
1272}
1273
1274/**
1275 * Returns the root element of the document, or NULL if the document is empty.
1276 * Const variant.
1277 * @return
1278 */
1279const ElementNode* Document::getRootElement() const
1280{
1281 return m->pRootElement;
1282}
1283
1284/**
1285 * Returns the root element of the document, or NULL if the document is empty.
1286 * Non-const variant.
1287 * @return
1288 */
1289ElementNode* Document::getRootElement()
1290{
1291 return m->pRootElement;
1292}
1293
1294/**
1295 * Creates a new element node and sets it as the root element. This will
1296 * only work if the document is empty; otherwise EDocumentNotEmpty is thrown.
1297 */
1298ElementNode* Document::createRootElement(const char *pcszRootElementName)
1299{
1300 if (m->pRootElement || m->plibDocument)
1301 throw EDocumentNotEmpty(RT_SRC_POS);
1302
1303 // libxml side: create document, create root node
1304 m->plibDocument = xmlNewDoc((const xmlChar*)"1.0");
1305 xmlNode *plibRootNode;
1306 if (!(plibRootNode = xmlNewNode(NULL, // namespace
1307 (const xmlChar*)pcszRootElementName)))
1308 throw std::bad_alloc();
1309 xmlDocSetRootElement(m->plibDocument, plibRootNode);
1310
1311 // now wrap this in C++
1312 m->pRootElement = new ElementNode(NULL, NULL, plibRootNode);
1313
1314 return m->pRootElement;
1315}
1316
1317////////////////////////////////////////////////////////////////////////////////
1318//
1319// XmlParserBase class
1320//
1321////////////////////////////////////////////////////////////////////////////////
1322
1323XmlParserBase::XmlParserBase()
1324{
1325 m_ctxt = xmlNewParserCtxt();
1326 if (m_ctxt == NULL)
1327 throw std::bad_alloc();
1328}
1329
1330XmlParserBase::~XmlParserBase()
1331{
1332 xmlFreeParserCtxt (m_ctxt);
1333 m_ctxt = NULL;
1334}
1335
1336////////////////////////////////////////////////////////////////////////////////
1337//
1338// XmlFileParser class
1339//
1340////////////////////////////////////////////////////////////////////////////////
1341
1342struct XmlFileParser::Data
1343{
1344 xmlParserCtxtPtr ctxt;
1345 iprt::MiniString strXmlFilename;
1346
1347 Data()
1348 {
1349 if (!(ctxt = xmlNewParserCtxt()))
1350 throw std::bad_alloc();
1351 }
1352
1353 ~Data()
1354 {
1355 xmlFreeParserCtxt(ctxt);
1356 ctxt = NULL;
1357 }
1358};
1359
1360XmlFileParser::XmlFileParser()
1361 : XmlParserBase(),
1362 m(new Data())
1363{
1364}
1365
1366XmlFileParser::~XmlFileParser()
1367{
1368 delete m;
1369 m = NULL;
1370}
1371
1372struct IOContext
1373{
1374 File file;
1375 iprt::MiniString error;
1376
1377 IOContext(const char *pcszFilename, File::Mode mode, bool fFlush = false)
1378 : file(mode, pcszFilename, fFlush)
1379 {
1380 }
1381
1382 void setError(const xml::Error &x)
1383 {
1384 error = x.what();
1385 }
1386
1387 void setError(const std::exception &x)
1388 {
1389 error = x.what();
1390 }
1391};
1392
1393struct ReadContext : IOContext
1394{
1395 ReadContext(const char *pcszFilename)
1396 : IOContext(pcszFilename, File::Mode_Read)
1397 {
1398 }
1399};
1400
1401struct WriteContext : IOContext
1402{
1403 WriteContext(const char *pcszFilename, bool fFlush)
1404 : IOContext(pcszFilename, File::Mode_Overwrite, fFlush)
1405 {
1406 }
1407};
1408
1409/**
1410 * Reads the given file and fills the given Document object with its contents.
1411 * Throws XmlError on parsing errors.
1412 *
1413 * The document that is passed in will be reset before being filled if not empty.
1414 *
1415 * @param strFilename in: name fo file to parse.
1416 * @param doc out: document to be reset and filled with data according to file contents.
1417 */
1418void XmlFileParser::read(const iprt::MiniString &strFilename,
1419 Document &doc)
1420{
1421 GlobalLock lock;
1422// global.setExternalEntityLoader(ExternalEntityLoader);
1423
1424 m->strXmlFilename = strFilename;
1425 const char *pcszFilename = strFilename.c_str();
1426
1427 ReadContext context(pcszFilename);
1428 doc.m->reset();
1429 if (!(doc.m->plibDocument = xmlCtxtReadIO(m->ctxt,
1430 ReadCallback,
1431 CloseCallback,
1432 &context,
1433 pcszFilename,
1434 NULL, // encoding = auto
1435 XML_PARSE_NOBLANKS)))
1436 throw XmlError(xmlCtxtGetLastError(m->ctxt));
1437
1438 doc.refreshInternals();
1439}
1440
1441// static
1442int XmlFileParser::ReadCallback(void *aCtxt, char *aBuf, int aLen)
1443{
1444 ReadContext *pContext = static_cast<ReadContext*>(aCtxt);
1445
1446 /* To prevent throwing exceptions while inside libxml2 code, we catch
1447 * them and forward to our level using a couple of variables. */
1448
1449 try
1450 {
1451 return pContext->file.read(aBuf, aLen);
1452 }
1453 catch (const xml::EIPRTFailure &err) { pContext->setError(err); }
1454 catch (const xml::Error &err) { pContext->setError(err); }
1455 catch (const std::exception &err) { pContext->setError(err); }
1456 catch (...) { pContext->setError(xml::LogicError(RT_SRC_POS)); }
1457
1458 return -1 /* failure */;
1459}
1460
1461int XmlFileParser::CloseCallback(void *aCtxt)
1462{
1463 /// @todo to be written
1464
1465 return -1;
1466}
1467
1468////////////////////////////////////////////////////////////////////////////////
1469//
1470// XmlFileWriter class
1471//
1472////////////////////////////////////////////////////////////////////////////////
1473
1474struct XmlFileWriter::Data
1475{
1476 Document *pDoc;
1477};
1478
1479XmlFileWriter::XmlFileWriter(Document &doc)
1480{
1481 m = new Data();
1482 m->pDoc = &doc;
1483}
1484
1485XmlFileWriter::~XmlFileWriter()
1486{
1487 delete m;
1488}
1489
1490void XmlFileWriter::writeInternal(const char *pcszFilename, bool fSafe)
1491{
1492 WriteContext context(pcszFilename, fSafe);
1493
1494 GlobalLock lock;
1495
1496 /* serialize to the stream */
1497 xmlIndentTreeOutput = 1;
1498 xmlTreeIndentString = " ";
1499 xmlSaveNoEmptyTags = 0;
1500
1501 xmlSaveCtxtPtr saveCtxt;
1502 if (!(saveCtxt = xmlSaveToIO(WriteCallback,
1503 CloseCallback,
1504 &context,
1505 NULL,
1506 XML_SAVE_FORMAT)))
1507 throw xml::LogicError(RT_SRC_POS);
1508
1509 long rc = xmlSaveDoc(saveCtxt, m->pDoc->m->plibDocument);
1510 if (rc == -1)
1511 {
1512 /* look if there was a forwared exception from the lower level */
1513// if (m->trappedErr.get() != NULL)
1514// m->trappedErr->rethrow();
1515
1516 /* there must be an exception from the Output implementation,
1517 * otherwise the save operation must always succeed. */
1518 throw xml::LogicError(RT_SRC_POS);
1519 }
1520
1521 xmlSaveClose(saveCtxt);
1522}
1523
1524void XmlFileWriter::write(const char *pcszFilename, bool fSafe)
1525{
1526 if (!fSafe)
1527 writeInternal(pcszFilename, fSafe);
1528 else
1529 {
1530 /* Empty string and directory spec must be avoid. */
1531 if (RTPathFilename(pcszFilename) == NULL)
1532 throw xml::LogicError(RT_SRC_POS);
1533
1534 /* Construct both filenames first to ease error handling. */
1535 char szTmpFilename[RTPATH_MAX];
1536 int rc = RTStrCopy(szTmpFilename, sizeof(szTmpFilename) - sizeof("-tmp") + 1, pcszFilename);
1537 if (RT_FAILURE(rc))
1538 throw EIPRTFailure(rc, "RTStrCopy");
1539 strcat(szTmpFilename, "-tmp");
1540
1541 char szPrevFilename[RTPATH_MAX];
1542 rc = RTStrCopy(szPrevFilename, sizeof(szPrevFilename) - sizeof("-prev") + 1, pcszFilename);
1543 if (RT_FAILURE(rc))
1544 throw EIPRTFailure(rc, "RTStrCopy");
1545 strcat(szPrevFilename, "-prev");
1546
1547 /* Write the XML document to the temporary file. */
1548 writeInternal(szTmpFilename, fSafe);
1549
1550 /* Make a backup of any existing file (ignore failure). */
1551 uint64_t cbPrevFile;
1552 rc = RTFileQuerySize(pcszFilename, &cbPrevFile);
1553 if (RT_SUCCESS(rc) && cbPrevFile >= 16)
1554 RTFileRename(pcszFilename, szPrevFilename, RTPATHRENAME_FLAGS_REPLACE);
1555
1556 /* Commit the temporary file. Just leave the tmp file behind on failure. */
1557 rc = RTFileRename(szTmpFilename, pcszFilename, RTPATHRENAME_FLAGS_REPLACE);
1558 if (RT_FAILURE(rc))
1559 throw EIPRTFailure(rc, "Failed to replace '%s' with '%s'", pcszFilename, szTmpFilename);
1560
1561 /* Flush the directory changes (required on linux at least). */
1562 RTPathStripFilename(szTmpFilename);
1563 rc = RTDirFlush(szTmpFilename);
1564 AssertMsg(RT_SUCCESS(rc) || rc == VERR_NOT_SUPPORTED || rc == VERR_NOT_IMPLEMENTED, ("%Rrc\n", rc));
1565 }
1566}
1567
1568int XmlFileWriter::WriteCallback(void *aCtxt, const char *aBuf, int aLen)
1569{
1570 WriteContext *pContext = static_cast<WriteContext*>(aCtxt);
1571
1572 /* To prevent throwing exceptions while inside libxml2 code, we catch
1573 * them and forward to our level using a couple of variables. */
1574 try
1575 {
1576 return pContext->file.write(aBuf, aLen);
1577 }
1578 catch (const xml::EIPRTFailure &err) { pContext->setError(err); }
1579 catch (const xml::Error &err) { pContext->setError(err); }
1580 catch (const std::exception &err) { pContext->setError(err); }
1581 catch (...) { pContext->setError(xml::LogicError(RT_SRC_POS)); }
1582
1583 return -1 /* failure */;
1584}
1585
1586int XmlFileWriter::CloseCallback(void *aCtxt)
1587{
1588 /// @todo to be written
1589
1590 return -1;
1591}
1592
1593
1594} // end namespace xml
1595
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette