| /******************************************************************************* |
| * Copyright (c) 2008-2011 Sonatype, Inc. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * Sonatype, Inc. - initial API and implementation |
| * Andrew Eisenberg - Work on Bug 350414 |
| *******************************************************************************/ |
| |
| package org.eclipse.m2e.core.ui.internal.editing; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| import org.w3c.dom.ProcessingInstruction; |
| import org.w3c.dom.Text; |
| |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.jface.text.DocumentRewriteSession; |
| import org.eclipse.jface.text.DocumentRewriteSessionType; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IDocumentExtension4; |
| import org.eclipse.wst.sse.core.StructuredModelManager; |
| import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; |
| import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; |
| import org.eclipse.wst.sse.core.internal.undo.IStructuredTextUndoManager; |
| import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument; |
| import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; |
| import org.eclipse.wst.xml.core.internal.provisional.format.FormatProcessorXML; |
| |
| |
| /** |
| * this class contains tools for editing the pom files using dom tree operations. |
| * |
| * @author mkleint |
| */ |
| @SuppressWarnings("restriction") |
| public class PomEdits { |
| |
| public static final String NAMESPACE = "http://maven.apache.org/POM/4.0.0"; //$NON-NLS-1$ |
| |
| public static final String NAMESPACE_LOCATION = "http://maven.apache.org/xsd/maven-4.0.0.xsd"; //$NON-NLS-1$ |
| |
| public static final String PROJECT = "project"; //$NON-NLS-1$ |
| |
| public static final String MODEL_VERSION = "modelVersion"; //$NON-NLS-1$ |
| |
| public static final String MODEL_VERSION_VALUE = "4.0.0"; //$NON-NLS-1$ |
| |
| public static final String DEPENDENCIES = "dependencies"; //$NON-NLS-1$ |
| |
| public static final String GROUP_ID = "groupId";//$NON-NLS-1$ |
| |
| public static final String ARTIFACT_ID = "artifactId"; //$NON-NLS-1$ |
| |
| public static final String DEPENDENCY = "dependency"; //$NON-NLS-1$ |
| |
| public static final String DEPENDENCY_MANAGEMENT = "dependencyManagement"; //$NON-NLS-1$ |
| |
| public static final String EXCLUSIONS = "exclusions"; //$NON-NLS-1$ |
| |
| public static final String EXCLUSION = "exclusion"; //$NON-NLS-1$ |
| |
| public static final String VERSION = "version"; //$NON-NLS-1$ |
| |
| public static final String PLUGIN = "plugin"; //$NON-NLS-1$ |
| |
| public static final String CONFIGURATION = "configuration";//$NON-NLS-1$ |
| |
| public static final String PLUGINS = "plugins";//$NON-NLS-1$ |
| |
| public static final String PLUGIN_MANAGEMENT = "pluginManagement";//$NON-NLS-1$ |
| |
| public static final String BUILD = "build";//$NON-NLS-1$ |
| |
| public static final String PARENT = "parent";//$NON-NLS-1$ |
| |
| public static final String RELATIVE_PATH = "relativePath";//$NON-NLS-1$ |
| |
| public static final String TYPE = "type";//$NON-NLS-1$ |
| |
| public static final String CLASSIFIER = "classifier";//$NON-NLS-1$ |
| |
| public static final String OPTIONAL = "optional";//$NON-NLS-1$ |
| |
| public static final String SCOPE = "scope";//$NON-NLS-1$ |
| |
| public static final String MODULES = "modules";//$NON-NLS-1$ |
| |
| public static final String MODULE = "module";//$NON-NLS-1$ |
| |
| public static final String PROFILE = "profile";//$NON-NLS-1$ |
| |
| public static final String ID = "id";//$NON-NLS-1$ |
| |
| public static final String NAME = "name"; //$NON-NLS-1$ |
| |
| public static final String URL = "url";//$NON-NLS-1$ |
| |
| public static final String DESCRIPTION = "description";//$NON-NLS-1$ |
| |
| public static final String INCEPTION_YEAR = "inceptionYear";//$NON-NLS-1$ |
| |
| public static final String ORGANIZATION = "organization"; //$NON-NLS-1$ |
| |
| public static final String SCM = "scm"; //$NON-NLS-1$ |
| |
| public static final String CONNECTION = "connection";//$NON-NLS-1$ |
| |
| public static final String DEV_CONNECTION = "developerConnection";//$NON-NLS-1$ |
| |
| public static final String TAG = "tag";//$NON-NLS-1$ |
| |
| public static final String ISSUE_MANAGEMENT = "issueManagement"; //$NON-NLS-1$ |
| |
| public static final String SYSTEM = "system"; //$NON-NLS-1$ |
| |
| public static final String SYSTEM_PATH = "systemPath"; //$NON-NLS-1$ |
| |
| public static final String CI_MANAGEMENT = "ciManagement"; //$NON-NLS-1$ |
| |
| public static final String PACKAGING = "packaging"; //$NON-NLS-1$ |
| |
| public static final String PROPERTIES = "properties"; //$NON-NLS-1$ |
| |
| public static final String EXTENSION = "extension"; //$NON-NLS-1$ |
| |
| public static final String EXTENSIONS = "extensions"; //$NON-NLS-1$ |
| |
| public static final String PROFILES = "profiles";//$NON-NLS-1$ |
| |
| public static final String EXECUTIONS = "executions"; //$NON-NLS-1$ |
| |
| public static final String EXECUTION = "execution";//$NON-NLS-1$ |
| |
| public static final String GOAL = "goal";//$NON-NLS-1$ |
| |
| public static final String GOALS = "goals";//$NON-NLS-1$ |
| |
| public static Element findChild(Element parent, String name) { |
| if(parent == null) { |
| return null; |
| } |
| NodeList rootList = parent.getChildNodes(); |
| for(int i = 0; i < rootList.getLength(); i++ ) { |
| Node nd = rootList.item(i); |
| if(nd instanceof Element) { |
| Element el = (Element) nd; |
| if(name.equals(el.getNodeName())) { |
| return el; |
| } |
| } |
| } |
| return null; |
| } |
| |
| public static List<Element> findChilds(Element parent, String name) { |
| List<Element> toRet = new ArrayList<Element>(); |
| if(parent != null) { |
| NodeList rootList = parent.getChildNodes(); |
| for(int i = 0; i < rootList.getLength(); i++ ) { |
| Node nd = rootList.item(i); |
| if(nd instanceof Element) { |
| Element el = (Element) nd; |
| if(name.equals(el.getNodeName())) { |
| toRet.add(el); |
| } |
| } |
| } |
| } |
| return toRet; |
| } |
| |
| public static String getTextValue(Node element) { |
| if(element == null) |
| return null; |
| StringBuffer buff = new StringBuffer(); |
| NodeList list = element.getChildNodes(); |
| for(int i = 0; i < list.getLength(); i++ ) { |
| Node child = list.item(i); |
| if(child instanceof Text) { |
| Text text = (Text) child; |
| buff.append(text.getData().trim()); //352416 the value is trimmed because of the multiline values |
| //that get trimmed by maven itself as well, any comparison to resolved model needs to do the trimming |
| // or risks false negative results. |
| } |
| } |
| return buff.toString(); |
| } |
| |
| /** |
| * finds exactly one (first) occurence of child element with the given name (eg. dependency) that fulfills conditions |
| * expressed by the Matchers (eg. groupId/artifactId match) |
| * |
| * @param parent |
| * @param name |
| * @param matchers |
| * @return |
| */ |
| public static Element findChild(Element parent, String name, Matcher... matchers) { |
| OUTTER: for(Element el : findChilds(parent, name)) { |
| for(Matcher match : matchers) { |
| if(!match.matches(el)) { |
| continue OUTTER; |
| } |
| } |
| return el; |
| } |
| return null; |
| } |
| |
| /** |
| * helper method, creates a subelement with text embedded. does not format the result. primarily to be used in cases |
| * like <code><goals><goal>xxx</goal></goals></code> |
| * |
| * @param parent |
| * @param name |
| * @param value |
| * @return |
| */ |
| public static Element createElementWithText(Element parent, String name, String value) { |
| Document doc = parent.getOwnerDocument(); |
| Element newElement = doc.createElement(name); |
| parent.appendChild(newElement); |
| newElement.appendChild(doc.createTextNode(value)); |
| return newElement; |
| } |
| |
| /** |
| * helper method, creates a subelement, does not format result. |
| * |
| * @param parent the parent element |
| * @param name the name of the new element |
| * @return the created element |
| */ |
| public static Element createElement(Element parent, String name) { |
| Document doc = parent.getOwnerDocument(); |
| Element newElement = doc.createElement(name); |
| parent.appendChild(newElement); |
| return newElement; |
| } |
| |
| /** |
| * sets text value to the given element. any existing text children are removed and replaced by this new one. |
| * |
| * @param element |
| * @param value |
| */ |
| public static void setText(Element element, String value) { |
| NodeList list = element.getChildNodes(); |
| List<Node> toRemove = new ArrayList<Node>(); |
| for(int i = 0; i < list.getLength(); i++ ) { |
| Node child = list.item(i); |
| if(child instanceof Text) { |
| toRemove.add(child); |
| } |
| } |
| for(Node rm : toRemove) { |
| element.removeChild(rm); |
| } |
| Document doc = element.getOwnerDocument(); |
| element.appendChild(doc.createTextNode(value)); |
| } |
| |
| /** |
| * unlike the findChild() equivalent, this one creates the element if not present and returns it. Therefore it shall |
| * only be invoked within the PomEdits.Operation |
| * |
| * @param parent |
| * @param names chain of element names to find/create |
| * @return |
| */ |
| public static Element getChild(Element parent, String... names) { |
| Element toFormat = null; |
| Element toRet = null; |
| if(names.length == 0) { |
| throw new IllegalArgumentException("At least one child name has to be specified"); |
| } |
| for(String name : names) { |
| toRet = findChild(parent, name); |
| if(toRet == null) { |
| toRet = parent.getOwnerDocument().createElement(name); |
| parent.appendChild(toRet); |
| if(toFormat == null) { |
| toFormat = toRet; |
| } |
| } |
| parent = toRet; |
| } |
| if(toFormat != null) { |
| format(toFormat); |
| } |
| return toRet; |
| } |
| |
| /** |
| * proper remove of a child element |
| */ |
| public static void removeChild(Element parent, Element child) { |
| if(child != null) { |
| Node prev = child.getPreviousSibling(); |
| if(prev instanceof Text) { |
| Text txt = (Text) prev; |
| int lastnewline = getLastEolIndex(txt.getData()); |
| if(lastnewline >= 0) { |
| txt.setData(txt.getData().substring(0, lastnewline)); |
| } |
| } |
| parent.removeChild(child); |
| } |
| } |
| |
| private static int getLastEolIndex(String s) { |
| if(s == null || s.length() == 0) { |
| return -1; |
| } |
| for(int i = s.length() - 1; i >= 0; i-- ) { |
| char c = s.charAt(i); |
| if(c == '\r') { |
| return i; |
| } |
| if(c == '\n') { |
| if(i > 0 && s.charAt(i - 1) == '\r') { |
| return i - 1; |
| } |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| /** |
| * remove the current element if it doesn't contain any sublements, useful for lists etc, works recursively removing |
| * all parents up that don't have any children elements. |
| * |
| * @param el |
| */ |
| public static void removeIfNoChildElement(Element el) { |
| NodeList nl = el.getChildNodes(); |
| boolean hasChilds = false; |
| for(int i = 0; i < nl.getLength(); i++ ) { |
| Node child = nl.item(i); |
| if(child instanceof Element) { |
| hasChilds = true; |
| } |
| } |
| if(!hasChilds) { |
| Node parent = el.getParentNode(); |
| if(parent != null && parent instanceof Element) { |
| removeChild((Element) parent, el); |
| removeIfNoChildElement((Element) parent); |
| } |
| } |
| } |
| |
| public static Element insertAt(Element newElement, int offset) { |
| Document doc = newElement.getOwnerDocument(); |
| if(doc instanceof IDOMDocument) { |
| IDOMDocument domDoc = (IDOMDocument) doc; |
| IndexedRegion ir = domDoc.getModel().getIndexedRegion(offset); |
| Node parent = ((Node) ir).getParentNode(); |
| if(ir instanceof Text) { |
| Text txt = (Text) ir; |
| String data = txt.getData(); |
| int dataSplitIndex = offset - ir.getStartOffset(); |
| String beforeText = data.substring(0, dataSplitIndex); |
| String afterText = data.substring(dataSplitIndex); |
| Text after = doc.createTextNode(afterText); |
| Text before = doc.createTextNode(beforeText); |
| parent.replaceChild(after, txt); |
| parent.insertBefore(newElement, after); |
| parent.insertBefore(before, newElement); |
| } else if(ir instanceof Element) { |
| if(ir.getStartOffset() == offset) { |
| // caret is before the tag, not within its bounds |
| parent.insertBefore(newElement, (Element) ir); |
| } else { |
| ((Element) ir).appendChild(newElement); |
| } |
| } else { |
| throw new IllegalArgumentException(); |
| } |
| } else { |
| throw new IllegalArgumentException(); |
| } |
| return newElement; |
| } |
| |
| /** |
| * finds the element at offset, if other type of node at offset, will return it's parent element (if any) |
| * |
| * @param doc |
| * @param offset |
| * @return |
| */ |
| public static Element elementAtOffset(Document doc, int offset) { |
| if(doc instanceof IDOMDocument) { |
| IDOMDocument domDoc = (IDOMDocument) doc; |
| IndexedRegion ir = domDoc.getModel().getIndexedRegion(offset); |
| if(ir instanceof Element) { |
| Element elem = (Element) ir; |
| if(ir.getStartOffset() == offset) { |
| // caret is before the tag, not within its bounds |
| elem = (Element) elem.getParentNode(); |
| } |
| return elem; |
| } |
| Node parent = ((Node) ir).getParentNode(); |
| if(parent instanceof Element) { |
| return (Element) parent; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * formats the node (and content). please make sure to only format the node you have created.. |
| * |
| * @param newNode |
| */ |
| public static void format(Node newNode) { |
| Node parentNode = newNode.getParentNode(); |
| if(parentNode != null && newNode.equals(parentNode.getLastChild())) { |
| //add a new line to get the newly generated content correctly formatted. |
| Document ownerDocument; |
| if(parentNode instanceof Document) { |
| ownerDocument = (Document) parentNode; |
| } else { |
| ownerDocument = parentNode.getOwnerDocument(); |
| } |
| parentNode.appendChild(ownerDocument.createTextNode("\n")); //$NON-NLS-1$ |
| } |
| FormatProcessorXML formatProcessor = new FormatProcessorXML(); |
| //ignore any line width settings, causes wrong formatting of <foo>bar</foo> |
| formatProcessor.getFormatPreferences().setLineWidth(2000); |
| formatProcessor.formatNode(newNode); |
| } |
| |
| /** |
| * performs an modifying operation on top the |
| * |
| * @param file |
| * @param operation |
| * @throws IOException |
| * @throws CoreException |
| */ |
| public static void performOnDOMDocument(PomEdits.OperationTuple... fileOperations) throws IOException, CoreException { |
| for(OperationTuple tuple : fileOperations) { |
| IDOMModel domModel = null; |
| //TODO we might want to attempt iterating opened editors and somehow initialize those |
| // that were not yet initialized. Then we could avoid saving a file that is actually opened, but was never used so far (after restart) |
| try { |
| DocumentRewriteSession session = null; |
| IStructuredTextUndoManager undo = null; |
| if(tuple.isReadOnly()) { |
| domModel = (IDOMModel) StructuredModelManager.getModelManager().getExistingModelForRead(tuple.getDocument()); |
| if(domModel == null) { |
| domModel = (IDOMModel) StructuredModelManager.getModelManager().getModelForRead( |
| (IStructuredDocument) tuple.getDocument()); |
| } |
| } else { |
| domModel = tuple.getModel() != null ? tuple.getModel() |
| : (tuple.getFile() != null ? (IDOMModel) StructuredModelManager.getModelManager().getModelForEdit( |
| tuple.getFile()) : (IDOMModel) StructuredModelManager.getModelManager().getExistingModelForEdit( |
| tuple.getDocument())); //existing shall be ok here.. |
| |
| //let the model know we make changes |
| domModel.aboutToChangeModel(); |
| undo = domModel.getStructuredDocument().getUndoManager(); |
| //let the document know we make changes |
| if(domModel.getStructuredDocument() instanceof IDocumentExtension4) { |
| IDocumentExtension4 ext4 = (IDocumentExtension4) domModel.getStructuredDocument(); |
| session = ext4.startRewriteSession(DocumentRewriteSessionType.UNRESTRICTED_SMALL); |
| } |
| undo.beginRecording(domModel); |
| |
| // fill with minimal pom content |
| Document doc = domModel.getDocument(); |
| if(doc.getDocumentElement() == null) { |
| |
| Node first = doc.getFirstChild(); |
| if(first == null || !(first instanceof ProcessingInstruction)) { |
| doc.insertBefore(doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\""), first); //$NON-NLS-1$ //$NON-NLS-2$ |
| doc.insertBefore(doc.createTextNode("\n"), first); //$NON-NLS-1$ |
| } |
| |
| Element project = doc.createElement(PROJECT); |
| project.setAttribute("xmlns", NAMESPACE); //$NON-NLS-1$ |
| project.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); //$NON-NLS-1$ //$NON-NLS-2$ |
| project.setAttribute("xsi:schemaLocation", NAMESPACE + " " + NAMESPACE_LOCATION); //$NON-NLS-1$ //$NON-NLS-2$ |
| doc.appendChild(project); |
| |
| Element modelVersion = doc.createElement(MODEL_VERSION); |
| modelVersion.appendChild(doc.createTextNode(MODEL_VERSION_VALUE)); //$NON-NLS-1$ |
| project.appendChild(modelVersion); |
| format(project); |
| } |
| } |
| |
| try { |
| tuple.getOperation().process(domModel.getDocument()); |
| } finally { |
| if(!tuple.isReadOnly()) { |
| undo.endRecording(domModel); |
| if(session != null && domModel.getStructuredDocument() instanceof IDocumentExtension4) { |
| IDocumentExtension4 ext4 = (IDocumentExtension4) domModel.getStructuredDocument(); |
| ext4.stopRewriteSession(session); |
| } |
| domModel.changedModel(); |
| } |
| } |
| } finally { |
| if(domModel != null) { |
| if(tuple.isReadOnly()) { |
| domModel.releaseFromRead(); |
| } else if(domModel.getId() != null) { // id will be null for files outside of workspace |
| //for ducuments saving shall generally only happen when the model is not held elsewhere (eg. in opened view) |
| //for files, save always |
| if(tuple.isForceSave() || domModel.getReferenceCountForEdit() == 1) { |
| domModel.save(); |
| } |
| domModel.releaseFromEdit(); |
| } |
| } |
| } |
| } |
| } |
| |
| public static final class OperationTuple { |
| private final PomEdits.Operation operation; |
| |
| private final IFile file; |
| |
| private final IDocument document; |
| |
| private final IDOMModel model; |
| |
| private boolean readOnly = false; |
| |
| private boolean forceSave = false; |
| |
| /** |
| * operation on top of IFile is always saved |
| * |
| * @param file |
| * @param operation |
| */ |
| public OperationTuple(IFile file, PomEdits.Operation operation) { |
| assert file != null; |
| assert operation != null; |
| this.file = file; |
| this.operation = operation; |
| document = null; |
| model = null; |
| forceSave = true; |
| } |
| |
| /** |
| * operation on top of IDocument is only saved when noone else is editing the document. |
| * |
| * @param document |
| * @param operation |
| */ |
| public OperationTuple(IDocument document, PomEdits.Operation operation) { |
| this(document, operation, false); |
| } |
| |
| /** |
| * operation on top of IDocument is only saved when noone else is editing the document. |
| * |
| * @param document |
| * @param operation |
| * @param readonly operation that doesn't modify the content. Will only get the read, not edit model, up to the user |
| * of the code to ensure no edits happen |
| */ |
| public OperationTuple(IDocument document, PomEdits.Operation operation, boolean readOnly) { |
| assert operation != null; |
| this.document = document; |
| this.operation = operation; |
| file = null; |
| model = null; |
| this.readOnly = readOnly; |
| } |
| |
| /** |
| * only use for unmanaged models |
| * |
| * @param model |
| * @param operation |
| */ |
| public OperationTuple(IDOMModel model, PomEdits.Operation operation) { |
| assert model != null; |
| this.operation = operation; |
| this.model = model; |
| document = null; |
| file = null; |
| } |
| |
| /** |
| * force saving the document after performing the operation |
| */ |
| public void setForceSave() { |
| forceSave = true; |
| } |
| |
| public boolean isForceSave() { |
| return forceSave; |
| } |
| |
| /** |
| * @return Returns the readOnly. |
| */ |
| public boolean isReadOnly() { |
| return readOnly; |
| } |
| |
| public IFile getFile() { |
| return file; |
| } |
| |
| public PomEdits.Operation getOperation() { |
| return operation; |
| } |
| |
| public IDocument getDocument() { |
| return document; |
| } |
| |
| public IDOMModel getModel() { |
| return model; |
| } |
| |
| } |
| |
| /** |
| * operation to perform on top of the DOM document. see performOnDOMDocument() |
| * |
| * @author mkleint |
| */ |
| public static interface Operation { |
| void process(Document document); |
| } |
| |
| /** |
| * an Operation instance that aggregates multiple operations and performs then in given order. |
| * |
| * @author mkleint |
| */ |
| public static final class CompoundOperation implements Operation { |
| |
| private final Operation[] operations; |
| |
| public CompoundOperation(Operation... operations) { |
| this.operations = operations; |
| } |
| |
| public void process(Document document) { |
| for(Operation oper : operations) { |
| oper.process(document); |
| } |
| } |
| } |
| |
| /** |
| * an interface for identifying child elements that fulfill conditions expressed by the matcher. |
| * |
| * @author mkleint |
| */ |
| public static interface Matcher { |
| /** |
| * returns true if the given element matches the condition. |
| * |
| * @param child |
| * @return |
| */ |
| boolean matches(Element element); |
| } |
| |
| public static Matcher childEquals(final String elementName, final String matchingValue) { |
| return new Matcher() { |
| public boolean matches(Element child) { |
| String toMatch = PomEdits.getTextValue(PomEdits.findChild(child, elementName)); |
| return toMatch != null && toMatch.trim().equals(matchingValue); |
| } |
| }; |
| } |
| |
| public static Matcher textEquals(final String matchingValue) { |
| return new Matcher() { |
| public boolean matches(Element child) { |
| String toMatch = PomEdits.getTextValue(child); |
| return toMatch != null && toMatch.trim().equals(matchingValue); |
| } |
| }; |
| } |
| |
| public static Matcher childMissingOrEqual(final String elementName, final String matchingValue) { |
| return new Matcher() { |
| public boolean matches(Element child) { |
| Element match = PomEdits.findChild(child, elementName); |
| if(match == null) { |
| return true; |
| } |
| String toMatch = PomEdits.getTextValue(match); |
| return toMatch != null && toMatch.trim().equals(matchingValue); |
| } |
| }; |
| } |
| |
| /** |
| * keeps internal state, needs to be recreated for each query, when used in conjunction with out matchers shall |
| * probably be placed last. |
| * |
| * @param elementName |
| * @param index |
| * @return |
| */ |
| public static Matcher childAt(final int index) { |
| return new Matcher() { |
| int count = 0; |
| |
| public boolean matches(Element child) { |
| if(count == index) { |
| return true; |
| } |
| count++ ; |
| return false; |
| } |
| }; |
| } |
| |
| } |