From dbf43f3cae27a726c22e319eca939872c83fb3d8 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Tue, 22 Sep 2020 16:52:39 -0400 Subject: [PATCH] Doclink for catalog entries with 'catalog' attr Any of the catalog entry elements that have the 'catalog' attribute to refer to another catalog are now doclinks. Use xml:base when making catalog doc links Doc links now work inside ``. xml:base is taken into account when calculating where a doc link should point in catalog files. Closes #845 Signed-off-by: David Thompson --- .../catalog/CatalogCatalogEntry.java | 51 +++++ .../extensions/catalog/CatalogEntry.java | 67 ++++++ .../extensions/catalog/CatalogUtils.java | 144 +++++++++++-- .../extensions/catalog/URICatalogEntry.java | 51 +++++ .../XMLCatalogDocumentLinkParticipant.java | 41 ++-- .../catalog/XMLCatalogDocumentLinkTest.java | 204 ++++++++++++++++++ 6 files changed, 528 insertions(+), 30 deletions(-) create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/CatalogCatalogEntry.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/CatalogEntry.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/URICatalogEntry.java diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/CatalogCatalogEntry.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/CatalogCatalogEntry.java new file mode 100644 index 000000000..b8de9afa3 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/CatalogCatalogEntry.java @@ -0,0 +1,51 @@ +/******************************************************************************* +* Copyright (c) 2020 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.catalog; + +import java.nio.file.Paths; + +import org.eclipse.lemminx.dom.DOMAttr; +import org.eclipse.lemminx.dom.DOMElement; +import org.eclipse.lemminx.dom.DOMRange; +import org.eclipse.lemminx.utils.StringUtils; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +/** + * Represents a catalog entry that uses the "catalog" attribute to reference an external document + */ +public class CatalogCatalogEntry extends CatalogEntry { + + public CatalogCatalogEntry(@NonNull String baseURI, DOMElement entryElement) { + super(baseURI, entryElement); + } + + @Override + public DOMRange getLinkRange() { + DOMAttr catalogAttr = CatalogUtils.getCatalogEntryCatalog(getEntryElement()); + if (catalogAttr == null) { + return null; + } + return catalogAttr.getNodeAttrValue(); + } + + @Override + public String getResolvedURI() { + DOMAttr catalogAttr = CatalogUtils.getCatalogEntryCatalog(getEntryElement()); + if (catalogAttr == null) { + return null; + } + String lastSegment = catalogAttr.getValue(); + if (StringUtils.isBlank(lastSegment)) { + return null; + } + return Paths.get(getBaseURI(), lastSegment).toString(); + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/CatalogEntry.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/CatalogEntry.java new file mode 100644 index 000000000..1bc737072 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/CatalogEntry.java @@ -0,0 +1,67 @@ +/******************************************************************************* +* Copyright (c) 2020 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.catalog; + +import org.eclipse.lemminx.dom.DOMElement; +import org.eclipse.lemminx.dom.DOMRange; + +/** + * Represents a catalog entry that references an external document + */ +public abstract class CatalogEntry { + + private final String baseURI; + private final DOMElement entryElement; + + /** + * + * @param baseURI 's xml:base + 's xml:base (if in a + * ) + * @param entryElement the element that corresponds with this catalog entry + */ + protected CatalogEntry(String baseURI, DOMElement entryElement) { + this.baseURI = baseURI; + this.entryElement = entryElement; + } + + /** + * Returns the base URI for this catalog entry + * + * @return the base URI for this catalog entry + */ + public String getBaseURI() { + return baseURI; + } + + /** + * Returns the element that corresponds with this catalog entry + * + * @return the element that corresponds with this catalog entry + */ + protected DOMElement getEntryElement() { + return entryElement; + } + + /** + * Returns the range in the document where the link to an external document is, + * or null if this catalog entry does not refer to an external document + * + * @return the range in the document where the link to an external document is + */ + public abstract DOMRange getLinkRange(); + + /** + * Returns the URI for the document that this catalog entry references, or null + * if this catalog entry does not refer to an external document + * + * @return the URI for the document that this catalog entry references + */ + public abstract String getResolvedURI(); +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/CatalogUtils.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/CatalogUtils.java index 8f8f1250a..8d2a020bd 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/CatalogUtils.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/CatalogUtils.java @@ -10,17 +10,19 @@ package org.eclipse.lemminx.extensions.catalog; +import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; import org.eclipse.lemminx.dom.DOMAttr; import org.eclipse.lemminx.dom.DOMDocument; import org.eclipse.lemminx.dom.DOMElement; import org.eclipse.lemminx.dom.DOMNode; import org.eclipse.lemminx.utils.DOMUtils; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; /** * Utility functions for working with XML catalog documents @@ -30,30 +32,39 @@ public class CatalogUtils { /** * The catalog entries that have a 'uri' attribute */ - private static final Collection CATALOG_NAMES = Arrays.asList("public", "system", "uri", "systemSuffix", + private static final Collection HAS_URI_ATTRIBUTE = Arrays.asList("public", "system", "uri", "systemSuffix", "uriSuffix"); + /** + * The catalog entries that have a 'catalog' attribute + */ + private static final Collection HAS_CATALOG_ATTRIBUTE = Arrays.asList("delegatePublic", "delegateSystem", + "delegateUri", "nextCatalog"); + + private static final String XML_BASE_ATTRIBUTE = "xml:base"; + private static final String CATALOG_ENTITY_NAME = "catalog"; + private static final String GROUP_ENTITY_NAME = "group"; + private static final String URI_ATTRIBUTE_NAME = "uri"; + private static final String CATALOG_ATTRIBUTE_NAME = "catalog"; + /** - * Returns a list of all the catalog entries that have the attribute 'uri', or - * an empty list if the document is not a catalog. + * Returns a list of all the catalog entries in the given document, or an empty + * list if the document is not a catalog. * - * @param document The document to collect the catalog entries from - * @return A list of all the catalog entries that have the attribute 'uri', or - * an empty list if the document is not a catalog. + * @return a list of all the catalog entries in the given document, or an empty + * list if the document is not a catalog. */ - public static List getCatalogEntries(DOMDocument document) { + public static List getCatalogEntries(DOMDocument document) { if (!DOMUtils.isCatalog(document)) { return Collections.emptyList(); } for (DOMNode n : document.getChildren()) { if (CATALOG_ENTITY_NAME.equals(n.getNodeName())) { - return n.getChildren().stream().filter(CatalogUtils::isCatalogURIEntry).map((el) -> { - return (DOMElement) el; - }).collect(Collectors.toList()); + return collectCatalogEntries((DOMElement) n); } } return Collections.emptyList(); @@ -72,15 +83,114 @@ public static DOMAttr getCatalogEntryURI(DOMElement element) { } /** - * Checks if this node is a catalog entry that is required to have the 'uri' - * attribute + * Returns the catalog attribute node of the given catalog entry or null if + * there is no catalog attribute + * + * @param element The catalog entry to get the catalog attribute of + * @return the catalog attribute node of the given catalog entry or null if + * there is no catalog attribute + */ + public static DOMAttr getCatalogEntryCatalog(DOMElement element) { + return element.getAttributeNode(CATALOG_ATTRIBUTE_NAME); + } + + /** + * Returns true if this element is a catalog entry that is required to have the + * 'uri' attribute and false otherwise * - * @param node The node to check - * @return true if this node is an catalog entry that is required to have the + * @param element The element to check + * @return true if this element is an catalog entry that is required to have the * 'uri' attribute and false otherwise */ - private static boolean isCatalogURIEntry(DOMNode node) { - return node.isElement() && CATALOG_NAMES.contains(node.getNodeName()); + private static boolean isCatalogEntryWithURI(DOMElement element) { + return HAS_URI_ATTRIBUTE.contains(element.getNodeName()); + } + + /** + * Returns true if this element is a catalog entry that is required to have the + * 'catalog' attribute and false otherwise + * + * @param element The element to check + * @return true if this element is an catalog entry that is required to have the + * 'catalog' attribute and false otherwise + */ + private static boolean isCatalogEntryWithCatalog(DOMElement element) { + return HAS_CATALOG_ATTRIBUTE.contains(element.getNodeName()); + } + + /** + * Returns true if the given element is a group element and false otherwise + * + * @param element The element to check + * @return true if the given element is a group element and false otherwise + */ + private static boolean isGroupCatalogEntry(DOMElement element) { + return GROUP_ENTITY_NAME.equals(element.getNodeName()); + } + + /** + * Get a list of all catalog entry elements that are in the given catalog + * + * @param catalog the catalog element to collect the entries for + * @return A list of all the catalog entities in this catalog + */ + private static List collectCatalogEntries(@NonNull DOMElement catalog) { + List entries = new ArrayList<>(); + String baseURI = catalog.getAttribute(XML_BASE_ATTRIBUTE); + baseURI = baseURI == null ? "" : baseURI; + for (DOMNode node : catalog.getChildren()) { + if (node.isElement()) { + DOMElement element = (DOMElement) node; + CatalogEntry catalogEntry = createCatalogEntry(baseURI, element); + if (catalogEntry != null) { + entries.add(catalogEntry); + } else if (isGroupCatalogEntry(element)) { + entries.addAll(collectGroupEntries(element, baseURI)); + } + } + } + return entries; + } + + /** + * Get a list of all catalog entry elements that are in the given group + * + * @param group the group element to collect the entries for + * @param baseURI the baseURI of the catalog + * @return A list of all the catalog entities in this group + */ + private static List collectGroupEntries(DOMElement group, @NonNull String baseURI) { + List entries = new ArrayList<>(); + String groupSegment = group.getAttribute(XML_BASE_ATTRIBUTE); + if (groupSegment != null) { + baseURI = Paths.get(baseURI, groupSegment).toString(); + } + for (DOMNode node : group.getChildren()) { + if (node.isElement()) { + DOMElement element = (DOMElement) node; + CatalogEntry catalogEntry = createCatalogEntry(baseURI, element); + if (catalogEntry != null) { + entries.add(catalogEntry); + } + } + } + return entries; + } + + /** + * Returns a catalog entry for the given element and null if a catalog entry can't be made + * + * @param baseURI the base URI of the catalog entry + * @param element the element to turn into a catalog entry + * @return a catalog entry for the given element and null if a catalog entry can't be made + */ + private static CatalogEntry createCatalogEntry(@NonNull String baseURI, DOMElement element) { + if (isCatalogEntryWithURI(element)) { + return new URICatalogEntry(baseURI, element); + } else if (isCatalogEntryWithCatalog(element)) { + return new CatalogCatalogEntry(baseURI, element); + } + return null; } } \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/URICatalogEntry.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/URICatalogEntry.java new file mode 100644 index 000000000..7437eacc9 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/URICatalogEntry.java @@ -0,0 +1,51 @@ +/******************************************************************************* +* Copyright (c) 2020 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.catalog; + +import java.nio.file.Paths; + +import org.eclipse.lemminx.dom.DOMAttr; +import org.eclipse.lemminx.dom.DOMElement; +import org.eclipse.lemminx.dom.DOMRange; +import org.eclipse.lemminx.utils.StringUtils; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; + +/** + * Represents a catalog entry that uses the "uri" attribute to reference an external document + */ +public class URICatalogEntry extends CatalogEntry { + + public URICatalogEntry(@NonNull String baseURI, DOMElement entryElement) { + super(baseURI, entryElement); + } + + @Override + public DOMRange getLinkRange() { + DOMAttr uriAttr = CatalogUtils.getCatalogEntryURI(getEntryElement()); + if (uriAttr == null) { + return null; + } + return uriAttr.getNodeAttrValue(); + } + + @Override + public String getResolvedURI() { + DOMAttr uriAttr = CatalogUtils.getCatalogEntryURI(getEntryElement()); + if (uriAttr == null) { + return null; + } + String lastSegment = uriAttr.getValue(); + if (StringUtils.isBlank(lastSegment)) { + return null; + } + return Paths.get(getBaseURI(), lastSegment).toString(); + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/XMLCatalogDocumentLinkParticipant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/XMLCatalogDocumentLinkParticipant.java index 1c5ba8d52..217ccc431 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/XMLCatalogDocumentLinkParticipant.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/XMLCatalogDocumentLinkParticipant.java @@ -17,12 +17,10 @@ import org.apache.xerces.impl.XMLEntityManager; import org.apache.xerces.util.URI.MalformedURIException; import org.eclipse.lemminx.commons.BadLocationException; -import org.eclipse.lemminx.dom.DOMAttr; import org.eclipse.lemminx.dom.DOMDocument; -import org.eclipse.lemminx.dom.DOMElement; -import org.eclipse.lemminx.dom.DOMNode; import org.eclipse.lemminx.services.extensions.IDocumentLinkParticipant; import org.eclipse.lemminx.utils.FilesUtils; +import org.eclipse.lemminx.utils.StringUtils; import org.eclipse.lemminx.utils.XMLPositionUtility; import org.eclipse.lsp4j.DocumentLink; @@ -35,26 +33,43 @@ public class XMLCatalogDocumentLinkParticipant implements IDocumentLinkParticipa @Override public void findDocumentLinks(DOMDocument document, List links) { - for (DOMElement entry : CatalogUtils.getCatalogEntries(document)) { - DOMAttr catalogReference = CatalogUtils.getCatalogEntryURI(entry); - DOMNode valueLocation = catalogReference.getNodeAttrValue(); - try { - String path = getResolvedLocation(FilesUtils.removeFileScheme(document.getDocumentURI()), - catalogReference.getValue()); - links.add(XMLPositionUtility.createDocumentLink(valueLocation, path, true)); - } catch (BadLocationException e) { - LOGGER.log(Level.SEVERE, "Creation of document link failed", e); + for (CatalogEntry catalogEntry : CatalogUtils.getCatalogEntries(document)) { + DocumentLink docLink = createDocumentLinkFromCatalogEntry(document, catalogEntry); + if (docLink != null) { + links.add(docLink); } } } + /** + * Return a DocumentLink for the given CatalogEntry or null if this fails + * + * @param document The document that the attribute belongs to + * @param catalogEntry The CatalogEntry that should be turned into a + * DocumentLink + * @return a document link that links the CatalogEntry to the external document + * it references or null otherwise + */ + private static DocumentLink createDocumentLinkFromCatalogEntry(DOMDocument document, CatalogEntry catalogEntry) { + try { + String path = getResolvedLocation(FilesUtils.removeFileScheme(document.getDocumentURI()), + catalogEntry.getResolvedURI()); + if (path != null && catalogEntry.getLinkRange() != null) { + return XMLPositionUtility.createDocumentLink(catalogEntry.getLinkRange(), path, true); + } + } catch (BadLocationException e) { + LOGGER.log(Level.SEVERE, "Creation of document link failed", e); + } + return null; + } + /** * Returns the expanded system location * * @return the expanded system location */ private static String getResolvedLocation(String documentURI, String location) { - if (location == null) { + if (StringUtils.isBlank(location)) { return null; } try { diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/catalog/XMLCatalogDocumentLinkTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/catalog/XMLCatalogDocumentLinkTest.java index add39131f..d6679b4f8 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/catalog/XMLCatalogDocumentLinkTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/catalog/XMLCatalogDocumentLinkTest.java @@ -102,4 +102,208 @@ public void testPublicEntryWithSpacesDocumentLink() { dl(r(1, 39, 1, 54), "src/test/resources/my schema.xsd")); } + @Test + public void testDelegatePublicEntry() { + String xml = "\n" + // + " \n" + // + ""; + testDocumentLinkFor(xml, CATALOG_PATH, // + dl(r(1, 27, 1, 54), "src/test/resources/catalogs/catalog-public.xml")); + } + + @Test + public void testDelegateSystemEntry() { + String xml = "\n" + // + " \n" + // + ""; + testDocumentLinkFor(xml, CATALOG_PATH, // + dl(r(1, 27, 1, 54), "src/test/resources/catalogs/catalog-public.xml")); + } + + @Test + public void testDelegateUriEntry() { + String xml = "\n" + // + " \n" + // + ""; + testDocumentLinkFor(xml, CATALOG_PATH, // + dl(r(1, 24, 1, 51), "src/test/resources/catalogs/catalog-public.xml")); + } + + @Test + public void testNextCatalogEntry() { + String xml = "\n" + // + " \n" + // + ""; + testDocumentLinkFor(xml, CATALOG_PATH, // + dl(r(1, 24, 1, 51), "src/test/resources/catalogs/catalog-public.xml")); + } + + @Test + public void testCatalogEntryWithoutCatalog() { + String xml = "\n" + // + " \n" + // + ""; + testDocumentLinkFor(xml, CATALOG_PATH); + } + + @Test + public void testCatalogEntryWithoutURI() { + String xml = "\n" + // + " \n" + // + ""; + testDocumentLinkFor(xml, CATALOG_PATH); + } + + @Test + public void testEmptyNextCatalogEntry() { + String xml = "\n" + // + " \n" + // + ""; + testDocumentLinkFor(xml, CATALOG_PATH); + } + + @Test + public void testBlankNextCatalogEntry() { + String xml = "\n" + // + " \n" + // + ""; + testDocumentLinkFor(xml, CATALOG_PATH); + } + + @Test + public void testEmptyPublicCatalogEntry() { + String xml = "\n" + // + " \n" + // + ""; + testDocumentLinkFor(xml, CATALOG_PATH); + } + + @Test + public void testBlankPublicCatalogEntry() { + String xml = "\n" + // + " \n" + // + ""; + testDocumentLinkFor(xml, CATALOG_PATH); + } + + @Test + public void testCatalogURIEntryWithXMLBase() { + String xml = "\n" + // + " \n" + // + ""; + testDocumentLinkFor(xml, CATALOG_PATH, // + dl(r(1, 39, 1, 46), "src/test/resources/dtd/svg.dtd")); + } + + @Test + public void testCatalogURIEntryWithXMLBaseRelativeReference() { + String xml = "\n" + // + " \n" + // + ""; + testDocumentLinkFor(xml, CATALOG_PATH, // + dl(r(1, 39, 1, 46), "src/test/resources/dtd/svg.dtd")); + } + + @Test + public void testCatalogURIEntryWithXMLBaseTrailingBackslash() { + String xml = "\n" + // + " \n" + // + ""; + testDocumentLinkFor(xml, CATALOG_PATH, // + dl(r(1, 39, 1, 46), "src/test/resources/dtd/svg.dtd")); + } + + @Test + public void testCatalogURIEntryGroupWithBase() { + String xml = "\n" + // + " \n" + // + " \n" + // + " \n" + // + ""; + testDocumentLinkFor(xml, CATALOG_PATH, // + dl(r(2, 41, 2, 48), "src/test/resources/dtd/svg.dtd")); + } + + @Test + public void testCatalogURIEntryGroupWithoutBase() { + String xml = "\n" + // + " \n" + // + " \n" + // + " \n" + // + ""; + testDocumentLinkFor(xml, CATALOG_PATH, // + dl(r(2, 41, 2, 52), "src/test/resources/dtd/svg.dtd")); + } + + @Test + public void testCatalogURIWithCatalogAndGroupBase() { + String xml = "\n" + // + " \n" + // + " \n" + // + " \n" + // + ""; + testDocumentLinkFor(xml, CATALOG_PATH, // + dl(r(2, 41, 2, 48), "src/test/resources/abc/def/svg.dtd")); + } + + @Test + public void testCatalogCatalogEntryWithXMLBase() { + String xml = "\n" + // + " \n" + // + ""; + testDocumentLinkFor(xml, CATALOG_PATH, // + dl(r(1, 24, 1, 43), "src/test/resources/catalog/catalog-liferay.xml")); + } + + @Test + public void testCatalogCatalogEntryWithXMLBaseRelativeReference() { + String xml = "\n" + // + " \n" + // + ""; + testDocumentLinkFor(xml, CATALOG_PATH, // + dl(r(1, 24, 1, 43), "src/test/resources/catalog/catalog-liferay.xml")); + } + + @Test + public void testCatalogCatalogEntryWithXMLBaseTrailingBackslash() { + String xml = "\n" + // + " \n" + // + ""; + testDocumentLinkFor(xml, CATALOG_PATH, // + dl(r(1, 24, 1, 43), "src/test/resources/catalog/catalog-liferay.xml")); + } + + @Test + public void testCatalogCatalogEntryGroupWithBase() { + String xml = "\n" + // + " \n" + // + " \n" + // + " \n" + // + ""; + testDocumentLinkFor(xml, CATALOG_PATH, // + dl(r(2, 26, 2, 45), "src/test/resources/catalog/catalog-liferay.xml")); + } + + @Test + public void testCatalogCatalogEntryGroupWithoutBase() { + String xml = "\n" + // + " \n" + // + " \n" + // + " \n" + // + ""; + testDocumentLinkFor(xml, CATALOG_PATH, // + dl(r(2, 26, 2, 53), "src/test/resources/catalog/catalog-liferay.xml")); + } + + @Test + public void testCatalogCatalogWithCatalogAndGroupBase() { + String xml = "\n" + // + " \n" + // + " \n" + // + " \n" + // + ""; + testDocumentLinkFor(xml, CATALOG_PATH, // + dl(r(2, 26, 2, 37), "src/test/resources/abc/def/catalog.xml")); + } + } \ No newline at end of file