/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2007-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.image.io.metadata;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import javax.imageio.metadata.IIOMetadataNode;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.geotools.resources.Classes;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.OptionalDependencies;
import org.geotools.util.UnsupportedImplementationException;
/**
* Base class for {@linkplain GeographicMetadata geographic metadata} parsers. This class
* provides convenience methods for encoding and decoding metadata information. A metadata
* root {@linkplain Node node} is specified at construction time, together with a path to
* the {@linkplain Element element} of interest. Example of valid paths:
* <p>
* <ul>
* <li>{@code "CoordinateReferenceSystem/Datum"}</li>
* <li>{@code "CoordinateReferenceSystem/CoordinateSystem"}</li>
* <li>{@code "GridGeometry/Envelope"}</li>
* </ul>
* <p>
* In addition, some elements contains an arbitrary amount of childs. The path to child
* elements can also be specified to the constructor. Examples (note that the constructor
* expects paths relative to the parent; we show absolute paths below for completness):
* <p>
* <ul>
* <li>{@code "CoordinateReferenceSystem/CoordinateSystem/Axis"}</li>
* <li>{@code "GridGeometry/Envelope/CoordinateValues"}</li>
* <li>{@code "SampleDimensions/SampleDimension"}</li>
* </ul>
*
* The {@code get} and {@code set} methods defined in this class will operate on the
* <cite>selected</cite> {@linkplain Element element}, which may be either the one
* specified at construction time, or one of its childs. The element can be selected
* by {@link #selectParent} (the default) or {@link #selectChild}.
* <p>
* The example below creates an accessor for a node called {@code "CoordinateSystem"}
* which is expected to have childs called {@code "Axis"}:
*
* <blockquote><pre>
* MetadataAccessor accessor = new MetadataAccessor(metadata,
* "CoordinateReferenceSystem/CoordinateSystem", "Axis");
*
* accessor.selectParent();
* String csName = accessor.getAttributeAsString("name");
*
* accessor.selectChild(0);
* String firstAxisName = accessor.getAttributeAsString("name");
* </pre></blockquote>
*
* @since 2.4
* @version $Id$
* @source $URL$
* @author Martin Desruisseaux
* @author Cédric Briançon
*/
public class MetadataAccessor {
/**
* The separator between names in a node path.
*/
private static final char SEPARATOR = '/';
/**
* The owner of this accessor.
*/
final GeographicMetadata metadata;
/**
* The parent of child {@linkplain Element elements}.
*/
private final Node parent;
/**
* The {@linkplain #childs} path. This is the {@code childPath} parameter
* given to the constructor.
*/
private final String childPath;
/**
* The list of child elements. May be empty but never null.
*/
private final List<Node> childs;
/**
* The current element, or {@code null} if not yet selected.
*
* @see #selectChild
* @see #currentElement()
*/
private transient Element current;
/**
* {@code true} if warnings are enabled.
*/
private transient boolean warningsEnabled = true;
/**
* Creates an accessor with the same parent and childs than the specified one. The two
* accessors will share the same {@linkplain Node metadata nodes} (including the list
* of childs), so change in one accessor will be immediately reflected in the other
* accessor. However each accessor can {@linkplain #selectChild select their child}
* independently.
* <p>
* The main purpose of this constructor is to create many views over the same list
* of childs, where each view {@linkplain #selectChild select} a different child.
*/
protected MetadataAccessor(final MetadataAccessor clone) {
metadata = clone.metadata;
parent = clone.parent;
childPath = clone.childPath;
childs = clone.childs;
}
/**
* Creates an accessor for the {@linkplain Element element} at the given path. Paths are
* separated by the {@code '/'} character. See {@linkplain MetadataAccessor class javadoc}
* for path examples.
*
* @param metadata The metadata node.
* @param parentPath The path to the {@linkplain Node node} of interest, or {@code null}
* if the {@code metadata} root node is directly the node of interest.
* @param childPath The path (relative to {@code parentPath}) to the child
* {@linkplain Element elements}, or {@code null} if none.
*/
@SuppressWarnings("fallthrough")
protected MetadataAccessor(final GeographicMetadata metadata, final String parentPath, final String childPath) {
this.metadata = metadata;
final Node root = metadata.getRootNode();
/*
* Fetchs the parent node and ensure that we got a singleton. If there is more nodes than
* expected, log a warning and pickup the first one. If there is no node, create a new one.
*/
final List<Node> childs = new ArrayList<Node>(4);
if (parentPath != null) {
listChilds(root, parentPath, 0, childs, true);
final int count = childs.size();
switch (count) {
default: {
warning("<init>", ErrorKeys.TOO_MANY_OCCURENCES_$2,
new Object[] {parentPath, count});
// Fall through for picking the first node.
}
case 1: {
parent = childs.get(0);
childs.clear();
break;
}
case 0: {
parent = appendChild(root, parentPath);
break;
}
}
} else {
parent = root;
}
/*
* Computes a full path to children. Searching from 'metadata' root node using 'path'
* should be identical to searching from 'parent' node using 'childPath', except in
* case of badly formed metadata where the parent node appears more than once.
*/
this.childPath = childPath;
if (childPath != null) {
final String path;
if (parentPath != null) {
path = parentPath + SEPARATOR + childPath;
} else {
path = childPath;
}
listChilds(root, path, 0, childs, false);
this.childs = childs;
} else {
this.childs = Collections.emptyList();
}
if (parent instanceof Element) {
current = (Element) parent;
}
}
/**
* Adds to the {@link #childs} list the child nodes at the given {@code path}.
* This method is for constructor implementation only and invokes itself recursively.
*
* @param parent The parent metadata node.
* @param path The path to the nodes or elements to insert into the list.
* @param base The offset in {@code path} for the next element name.
* @param childs The list where to insert the nodes or elements.
* @param includeNodes {@code true} of adding nodes as well as elements.
*/
private static void listChilds(final Node parent, final String path, final int base,
final List<Node> childs, final boolean includeNodes)
{
final int upper = path.indexOf(SEPARATOR, base);
final String name = ((upper >= 0) ? path.substring(base, upper)
: path.substring(base)).trim();
final NodeList list = parent.getChildNodes();
final int length = list.getLength();
for (int i=0; i<length; i++) {
final Node candidate = list.item(i);
if (name.equals(candidate.getNodeName())) {
if (upper >= 0) {
listChilds(candidate, path, upper+1, childs, includeNodes);
} else if (includeNodes || (candidate instanceof Element)) {
// For the very last node, we may require an element.
childs.add(candidate);
}
}
}
}
/**
* Appends a child to the given parent.
*
* @param parent The parent to add a child to.
* @param path The path of the child to add.
* @return element The new child.
*/
private static Node appendChild(Node parent, final String path) {
int lower = 0;
search: for (int upper; (upper = path.indexOf(SEPARATOR, lower)) >= 0; lower=upper+1) {
final String name = path.substring(lower, upper).trim();
final NodeList list = parent.getChildNodes();
final int length = list.getLength();
for (int i=length; --i>=0;) {
final Node candidate = list.item(i);
if (name.equals(candidate.getNodeName())) {
parent = candidate;
continue search;
}
}
parent = parent.appendChild(new IIOMetadataNode(name.intern()));
}
final String name = path.substring(lower).trim().intern();
return parent.appendChild(new IIOMetadataNode(name));
}
/**
* Returns the number of child {@linkplain Element elements}.
* This is the upper value (exclusive) for {@link #selectChild}.
*
* @return The child {@linkplain Element elements} count.
*
* @see #selectChild
* @see #appendChild
*/
protected int childCount() {
return childs.size();
}
/**
* Adds a new child {@linkplain Element element} at the path given at construction time.
* The {@linkplain #childCount child count} will be increased by 1.
* <p>
* The new child is <strong>not</strong> automatically selected. In order to select this
* new child, the {@link #selectChild} method must be invoked explicitly.
*
* @return The index of the new child element.
*
* @see #childCount
* @see #selectChild
*/
protected int appendChild() {
final int size = childs.size();
final Node child = appendChild(parent, childPath);
if (child instanceof Element) {
childs.add((Element) child);
return size;
} else {
throw new UnsupportedImplementationException(child.getClass());
}
}
/**
* Selects the {@linkplain Element element} at the given index. Every subsequent calls
* to {@code get} or {@code set} methods will apply to this selected child element.
*
* @param index The index of the element to select.
* @throws IndexOutOfBoundsException if the specified index is out of bounds.
*
* @see #childCount
* @see #appendChild
* @see #selectParent
*/
protected void selectChild(final int index) throws IndexOutOfBoundsException {
current = (Element) childs.get(index);
}
/**
* Selects the <em>parent</em> of child elements. Every subsequent calls to {@code get}
* or {@code set} methods will apply to this parent element.
*
* @throws NoSuchElementException if there is no parent {@linkplain Element element}.
*
* @see #selectChild
*/
protected void selectParent() throws NoSuchElementException {
if (parent instanceof Element) {
current = (Element) parent;
} else {
throw new NoSuchElementException();
}
}
/**
* Returns the current element.
*
* @return The currently selected element.
* @throws IllegalStateException if there is no selected element.
*
* @see #selectChild
*/
private Element currentElement() throws IllegalStateException {
if (current == null) {
throw new IllegalStateException();
}
return current;
}
/**
* Returns the {@linkplain IIOMetadataNode#getUserObject user object} associated with the
* {@linkplain #selectChild selected element}, or {@code null} if none. If no user object
* is defined for the element, then the {@linkplain Node#getNodeValue node value} is returned
* as a fallback. This is consistent with {@link #setUserObject} implementation, and allows
* some parsing of nodes that are not {@link IIOMetadataNode} instances.
* <p>
* The {@code getUserObject} methods are the only ones to not parse the value returned by
* {@link #getAttributeAsString}.
*
* @return The user object, or {@code null} if none.
*
* @see #getUserObject(Class)
* @see #setUserObject
*/
protected Object getUserObject() {
final Element element = currentElement();
if (element instanceof IIOMetadataNode) {
final Object candidate = ((IIOMetadataNode) element).getUserObject();
if (candidate != null) {
return candidate;
}
}
/*
* getNodeValue() returns a String. We use it as a fallback, but in typical
* IIOMetadataNode usage this value is not used (according its javadoc), so
* it will often be null.
*/
return element.getNodeValue();
}
/**
* Returns the user object associated as an instance of the specified class. If the value
* returned by {@link #getUserObject()} is not of the expected type, then this method will
* tries to parse it as a string.
*
* @param type The expected class.
* @return The user object, or {@code null} if none.
* @throws ClassCastException if the user object can not be casted to the specified type.
*
* @see #getUserObject()
* @see #setUserObject
*/
protected <T> T getUserObject(Class<? extends T> type) throws ClassCastException {
type = Classes.primitiveToWrapper(type).asSubclass(type);
Object value = getUserObject();
if (value instanceof CharSequence) {
if (Number.class.isAssignableFrom(type)) {
value = Classes.valueOf(type, value.toString());
} else {
final Class<?> component = Classes.primitiveToWrapper(type.getComponentType());
if (Double.class.equals(component)) {
value = parseSequence(value.toString(), false, false);
} else if (Integer.class.equals(component)) {
value = parseSequence(value.toString(), false, true);
}
}
}
return type.cast(value);
}
/**
* Sets the {@linkplain IIOMetadataNode#setUserObject user object} associated with the
* {@linkplain #selectChild selected element}. This is the only {@code set} method that
* doesn't invoke {@link #setAttributeAsString} with a formatted value.
* <p>
* If the specified value is formattable (i.e. is a {@linkplain CharSequence character
* sequence}, a {@linkplain Number number} or an array of the above), then this method
* also {@linkplain IIOMetadataNode#setNodeValue sets the node value} as a string. This
* is mostly a convenience for formatting purpose since {@link IIOMetadataNode} don't
* use the node value. But it may help some libraries that are not designed to work with
* with user objects, since they are particular to Image I/O metadata.
*
* @param value The user object, or {@code null} if none.
* @throws UnsupportedImplementationException if the selected element is not an instance of
* {@link IIOMetadataNode}.
*
* @see #getUserObject()
*/
protected void setUserObject(final Object value) throws UnsupportedImplementationException {
final Element element = currentElement();
String asText = null;
if (value != null) {
final Class<?> type = value.getClass();
if (isFormattable(type)) {
asText = value.toString();
} else if (isFormattable(type.getComponentType())) {
asText = formatSequence(value);
}
}
if (element instanceof IIOMetadataNode) {
((IIOMetadataNode) element).setUserObject(value);
} else if (value!=null && asText==null) {
throw new UnsupportedImplementationException(Errors.format(ErrorKeys.ILLEGAL_CLASS_$2,
Classes.getClass(element), IIOMetadataNode.class));
}
element.setNodeValue(asText);
}
/**
* Returns {@code true} if values of the specified type can be formatted as a
* text. We allows formatting only for reasonably cheap objects, for example
* a Number but not a CoordinateReferenceSystem.
*/
private static boolean isFormattable(final Class<?> type) {
return CharSequence.class.isAssignableFrom(type) ||
Number.class.isAssignableFrom(Classes.primitiveToWrapper(type));
}
/**
* Returns an attribute as a string for the {@linkplain #selectChild selected element},
* or {@code null} if none. This method never returns an empty string.
* <p>
* Every {@code get} methods in this class except {@link #getUserObject getUserObject}
* invoke this method first. Consequently, this method provides a single point for
* overriding if subclasses want to process the attribute before parsing.
*
* @param attribute The attribute to fetch (e.g. {@code "name"}).
* @return The attribute value (never an empty string), or {@code null} if none.
*/
protected String getAttributeAsString(final String attribute) {
String candidate = currentElement().getAttribute(attribute);
if (candidate != null) {
candidate = candidate.trim();
if (candidate.length() == 0) {
candidate = null;
}
}
return candidate;
}
/**
* Set the attribute to the specified value,
* or remove the attribute if the value is null.
* <p>
* Every {@code set} methods in this class except {@link #setUserObject setUserObject}
* invoke this method last. Consequently, this method provides a single point for
* overriding if subclasses want to process the attribute after formatting.
*
* @param attribute The attribute name.
* @param value The attribute value.
*/
protected void setAttributeAsString(final String attribute, String value) {
final Element element = currentElement();
if (value == null || (value=value.trim()).length() == 0) {
if (element.hasAttribute(attribute)) {
element.removeAttribute(attribute);
}
} else {
element.setAttribute(attribute, value);
}
}
/**
* Set the attribute to the specified enumeration value,
* or remove the attribute if the value is null.
*
* @param attribute The attribute name.
* @param value The attribute value.
* @param enums The set of allowed values, or {@code null} if unknown.
*/
final void setAttributeAsEnum(final String attribute, String value, final Collection enums) {
if (value != null) {
value = value.replace('_', ' ').trim();
for (final Iterator it=enums.iterator(); it.hasNext();) {
final String e = (String) it.next();
if (value.equalsIgnoreCase(e)) {
value = e;
break;
}
}
}
setAttributeAsString(attribute, value);
}
/**
* Returns an attribute as an integer for the {@linkplain #selectChild selected element},
* or {@code null} if none. If the attribute can't be parsed as an integer, then this method
* logs a warning and returns {@code null}.
*
* @param attribute The attribute to fetch (e.g. {@code "minimum"}).
* @return The attribute value, or {@code null} if none or unparseable.
*/
protected Integer getAttributeAsInteger(final String attribute) {
String value = getAttributeAsString(attribute);
if (value != null) {
value = trimFractionalPart(value);
try {
return Integer.valueOf(value);
} catch (NumberFormatException e) {
warning("getAttributeAsInteger", ErrorKeys.UNPARSABLE_NUMBER_$1, value);
}
}
return null;
}
/**
* Set the attribute to the specified integer value.
*
* @param attribute The attribute name.
* @param value The attribute value.
*/
protected void setAttributeAsInteger(final String attribute, final int value) {
setAttributeAsString(attribute, Integer.toString(value));
}
/**
* Returns an attribute as an array of integers for the {@linkplain #selectChild selected
* element}, or {@code null} if none. If an element can't be parsed as an integer, then this
* method logs a warning and returns {@code null}.
*
* @param attribute The attribute to fetch (e.g. {@code "minimum"}).
* @param unique {@code true} if duplicated values should be collapsed into unique values,
* or {@code false} for preserving duplicated values.
* @return The attribute values, or {@code null} if none.
*/
protected int[] getAttributeAsIntegers(final String attribute, final boolean unique) {
return (int[]) parseSequence(getAttributeAsString(attribute), unique, true);
}
/**
* Set the attribute to the specified array of values,
* or remove the attribute if the array is {@code null}.
*
* @param attribute The attribute name.
* @param value The attribute value.
*/
protected void setAttributeAsIntegers(final String attribute, final int[] values) {
setAttributeAsString(attribute, formatSequence(values));
}
/**
* Returns an attribute as a floating point for the {@linkplain #selectChild selected element},
* or {@code null} if none. If the attribute can't be parsed as a floating point, then this
* method logs a warning and returns {@code null}.
*
* @param attribute The attribute to fetch (e.g. {@code "minimum"}).
* @return The attribute value, or {@code null} if none or unparseable.
*/
protected Double getAttributeAsDouble(final String attribute) {
final String value = getAttributeAsString(attribute);
if (value != null) try {
return Double.valueOf(value);
} catch (NumberFormatException e) {
warning("getAttributeAsDouble", ErrorKeys.UNPARSABLE_NUMBER_$1, value);
}
return null;
}
/**
* Set the attribute to the specified floating point value,
* or remove the attribute if the value is NaN.
*
* @param attribute The attribute name.
* @param value The attribute value.
*/
protected void setAttributeAsDouble(final String attribute, final double value) {
String text = null;
if (!Double.isNaN(value) && !Double.isInfinite(value)) {
text = Double.toString(value);
}
setAttributeAsString(attribute, text);
}
/**
* Returns an attribute as an array of floating point for the {@linkplain #selectChild
* selected element}, or {@code null} if none. If an element can't be parsed as a floating
* point, then this method logs a warning and returns {@code null}.
*
* @param attribute The attribute to fetch (e.g. {@code "fillValues"}).
* @param unique {@code true} if duplicated values should be collapsed into unique values,
* or {@code false} for preserving duplicated values.
* @return The attribute values, or {@code null} if none.
*/
protected double[] getAttributeAsDoubles(final String attribute, final boolean unique) {
return (double[]) parseSequence(getAttributeAsString(attribute), unique, false);
}
/**
* Set the attribute to the specified array of values,
* or remove the attribute if the array is {@code null}.
*
* @param attribute The attribute name.
* @param value The attribute value.
*/
protected void setAttributeAsDoubles(final String attribute, final double[] values) {
setAttributeAsString(attribute, formatSequence(values));
}
/**
* Implementation of {@link #getAttributeAsIntegers} and {@link #getAttributeAsDoubles} methods.
*
* @param sequence The sequence to parse.
* @param unique {@code true} if duplicated values should be collapsed into unique values,
* or {@code false} for preserving duplicated values.
* @param integers {@code true} for parsing as {@code int}, or {@code false} for parsing as
* {@code double}.
* @return The attribute values, or {@code null} if none.
*/
private Object parseSequence(final String sequence, final boolean unique, final boolean integers) {
if (sequence == null) {
return null;
}
final Collection<Number> numbers;
if (unique) {
numbers = new LinkedHashSet<Number>();
} else {
numbers = new ArrayList<Number>();
}
final StringTokenizer tokens = new StringTokenizer(sequence);
while (tokens.hasMoreTokens()) {
final String token = tokens.nextToken();
final Number number;
try {
if (integers) {
number = Integer.valueOf(token);
} else {
number = Double.valueOf(token);
}
} catch (NumberFormatException e) {
warning(integers ? "getAttributeAsIntegers" : "getAttributeAsDoubles", ErrorKeys.UNPARSABLE_NUMBER_$1, token);
continue;
}
numbers.add(number);
}
int count = 0;
final Object values;
if (integers) {
values = new int[numbers.size()];
} else {
values = new double[numbers.size()];
}
for (final Iterator it=numbers.iterator(); it.hasNext();) {
Array.set(values, count++, it.next());
}
assert Array.getLength(values) == count;
return values;
}
/**
* Formats a sequence for {@link #setAttributeAsIntegers} and {@link #setAttributeAsDoubles} implementations.
*
* @param value The attribute value.
* @return The formatted sequence.
*/
private static String formatSequence(final Object values) {
String text = null;
if (values != null) {
final StringBuilder buffer = new StringBuilder();
final int length = Array.getLength(values);
for (int i=0; i<length; i++) {
if (i != 0) {
buffer.append(' ');
}
buffer.append(Array.get(values, i));
}
text = buffer.toString();
}
return text;
}
/**
* Returns an attribute as a date for the {@linkplain #selectChild selected element},
* or {@code null} if none. If the attribute can't be parsed as a date, then this method
* logs a warning and returns {@code null}.
*
* @param attribute The attribute to fetch (e.g. {@code "origin"}).
* @return The attribute value, or {@code null} if none or unparseable.
*/
protected Date getAttributeAsDate(final String attribute) {
String value = getAttributeAsString(attribute);
if (value != null) {
value = trimFractionalPart(value);
return metadata.dateFormat().parse(value);
}
return null;
}
/**
* Set the attribute to the specified value, or remove the attribute if the value is null.
*
* @param attribute The attribute name.
* @param value The attribute value.
*/
protected void setAttributeAsDate(final String attribute, final Date value) {
String text = null;
if (value != null) {
text = metadata.dateFormat().format(value);
}
setAttributeAsString(attribute, text);
}
/**
* Trims the factional part of the given string, provided that it doesn't change the value.
* More specifically, this method removes the trailing {@code ".0"} characters if any. This
* method is automatically invoked before to {@linkplain #getAttributeAsInteger parse an integer} or to
* {@linkplain #getAttributeAsDate parse a date} (for simplifying fractional seconds).
*
* @param value The value to trim.
* @return The value without the trailing {@code ".0"} part.
*/
public static String trimFractionalPart(String value) {
value = value.trim();
for (int i=value.length(); --i>=0;) {
switch (value.charAt(i)) {
case '0': continue;
case '.': return value.substring(0, i);
default : return value;
}
}
return value;
}
/**
* Convenience method for logging a warning. Do not allow overriding, because
* it would not work for warnings emitted by the {@link #getAttributeAsDate} method.
*/
final void warning(final String method, final int key, final Object value) {
if (warningsEnabled) {
final LogRecord record = Errors.getResources(metadata.getLocale()).
getLogRecord(Level.WARNING, key, value);
record.setSourceClassName(MetadataAccessor.class.getName());
record.setSourceMethodName(method);
warningOccurred(record);
}
}
/**
* Invoked when a warning occured. This method is invoked when some inconsistency has
* been detected in the geographic metadata. The default implementation delegates
* to {@link GeographicMetadata#warningOccurred}.
*/
protected void warningOccurred(final LogRecord record) {
if (warningsEnabled) {
metadata.warningOccurred(record);
}
}
/**
* Enables or disables the warnings. Warnings are enabled by default. Subclasses way want
* to temporarily disable the warnings when failures are expected as the normal behavior.
* For example a subclass may invokes {@link #getAttributeAsInteger} and fallbacks on {@link #getAttributeAsDouble}
* if the former failed. In such case, the warnings should be disabled for the integer parsing,
* but not for the floating point parsing.
*
* @param enabled {@code true} for enabling warnings, or {@code false} for disabling.
* @return The previous state before this method has been invoked.
*/
protected boolean setWarningsEnabled(final boolean enabled) {
final boolean old = warningsEnabled;
warningsEnabled = enabled;
return old;
}
/**
* Returns a string representation of metadata, mostly for debugging purpose.
*/
@Override
public String toString() {
return OptionalDependencies.toString(OptionalDependencies.xmlToSwing(parent));
}
}