001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2007, GeoTools Project Managment Committee (PMC)
005: *
006: * This library is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU Lesser General Public
008: * License as published by the Free Software Foundation;
009: * version 2.1 of the License.
010: *
011: * This library is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * Lesser General Public License for more details.
015: */
016: package org.geotools.metadata;
017:
018: // J2SE dependencies
019: import java.lang.reflect.Array;
020: import java.lang.reflect.InvocationTargetException;
021: import java.lang.reflect.Method;
022: import java.lang.reflect.UndeclaredThrowableException;
023: import java.util.Collection;
024: import java.util.HashMap;
025: import java.util.Map;
026:
027: // Geotools implementation
028: import org.geotools.resources.XArray;
029: import org.geotools.resources.i18n.ErrorKeys;
030: import org.geotools.resources.i18n.Errors;
031: import org.geotools.resources.Utilities;
032:
033: /**
034: * The getters declared in a GeoAPI interface, together with setters (if any)
035: * declared in the Geotools implementation.
036: *
037: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/metadata/src/main/java/org/geotools/metadata/PropertyAccessor.java $
038: * @version $Id: PropertyAccessor.java 25193 2007-04-18 13:37:38Z desruisseaux $
039: * @author Martin Desruisseaux
040: */
041: final class PropertyAccessor {
042: /**
043: * The prefix for getters on boolean values.
044: */
045: private static final String IS = "is";
046:
047: /**
048: * The prefix for getters (general case).
049: */
050: private static final String GET = "get";
051:
052: /**
053: * The prefix for setters.
054: */
055: private static final String SET = "set";
056:
057: /**
058: * Methods to exclude from {@link #getGetters}. They are method inherited from
059: * {@link java.lang.Object}. Some of them, especially {@link Object#hashCode()}
060: * {@link Object#toString()} and {@link Object#clone()}, may be declared explicitly
061: * in some interface with a formal contract. Note: only no-argument methods need to
062: * be declared in this list.
063: */
064: private static final String[] EXCLUDES = { "clone", "finalize",
065: "getClass", "hashCode", "notify", "notifyAll", "toString",
066: "wait" };
067:
068: /**
069: * Getters shared between many instances of this class. Two different implementations
070: * may share the same getters but different setters.
071: */
072: private static final Map/*<Class, Method[]>*/SHARED_GETTERS = new HashMap();
073:
074: /**
075: * The implemented metadata interface.
076: */
077: final Class type;
078:
079: /**
080: * The implementation class. The following condition must hold:
081: *
082: * <blockquote><pre>
083: * type.{@linkplain Class#isAssignableFrom isAssignableFrom}(implementation);
084: * </pre></blockquote>
085: */
086: final Class implementation;
087:
088: /**
089: * The getter methods. This array should not contain any null element.
090: */
091: private final Method[] getters;
092:
093: /**
094: * The corresponding setter methods, or {@code null} if none. This array must have
095: * the same length than {@link #getters}. For every {@code getters[i]} element,
096: * {@code setters[i]} is the corresponding setter or {@code null} if there is none.
097: */
098: private final Method[] setters;
099:
100: /**
101: * Creates a new property reader for the specified metadata implementation.
102: *
103: * @param metadata The metadata implementation to wrap.
104: * @param type The interface implemented by the metadata.
105: * Should be the value returned by {@link #getType}.
106: */
107: PropertyAccessor(final Class implementation, final Class type) {
108: this .implementation = implementation;
109: this .type = type;
110: assert type.isAssignableFrom(implementation) : implementation;
111: getters = getGetters(type);
112: Method[] setters = null;
113: final Class[] arguments = new Class[1];
114: for (int i = 0; i < getters.length; i++) {
115: final Method getter = getters[i];
116: final Method setter; // To be determined later
117: arguments[0] = getter.getReturnType();
118: String name = getter.getName();
119: final int base = prefix(name).length();
120: if (name.length() > base) {
121: final char lo = name.charAt(base);
122: final char up = Character.toUpperCase(lo);
123: if (lo != up) {
124: name = SET + up + name.substring(base + 1);
125: } else {
126: name = SET + name.substring(base);
127: }
128: }
129: try {
130: setter = implementation.getMethod(name, arguments);
131: } catch (NoSuchMethodException e) {
132: continue;
133: }
134: if (setters == null) {
135: setters = new Method[getters.length];
136: }
137: setters[i] = setter;
138: }
139: this .setters = setters;
140: }
141:
142: /**
143: * Returns the metadata interface implemented by the specified implementation.
144: * Only one metadata interface can be implemented.
145: *
146: * @param metadata The metadata implementation to wraps.
147: * @param interfacePackage The root package for metadata interfaces.
148: * @return The single interface, or {@code null} if none where found.
149: */
150: static Class getType(final Class implementation,
151: final String interfacePackage) {
152: if (!implementation.isInterface()) {
153: final Class[] interfaces = implementation.getInterfaces();
154: int count = 0;
155: for (int i = 0; i < interfaces.length; i++) {
156: final Class candidate = interfaces[i];
157: if (candidate.getName().startsWith(interfacePackage)) {
158: interfaces[count++] = candidate;
159: }
160: }
161: if (count == 1) {
162: return interfaces[0];
163: }
164: }
165: return null;
166: }
167:
168: /**
169: * Returns the getters. The returned array should never be modified,
170: * since it may be shared among many instances of {@code PropertyAccessor}.
171: *
172: * @todo Ignore deprecated methods when we will be allowed to compile for J2SE 1.5.
173: */
174: private static Method[] getGetters(final Class type) {
175: synchronized (SHARED_GETTERS) {
176: Method[] getters = (Method[]) SHARED_GETTERS.get(type);
177: if (getters == null) {
178: getters = type.getMethods();
179: int count = 0;
180: for (int i = 0; i < getters.length; i++) {
181: final Method candidate = getters[i];
182: if (!candidate.getReturnType().equals(Void.TYPE)
183: && candidate.getParameterTypes().length == 0) {
184: /*
185: * We do not require a name starting with "get" or "is" prefix because some
186: * methods do not begin with such prefix, as in "ConformanceResult.pass()".
187: * Consequently we must provide special cases for no-arg methods inherited
188: * from java.lang.Object because some interfaces declare explicitly the
189: * contract for those methods.
190: */
191: final String name = candidate.getName();
192: if (!name.startsWith(SET) && !isExcluded(name)) {
193: getters[count++] = candidate;
194: }
195: }
196: }
197: getters = (Method[]) XArray.resize(getters, count);
198: SHARED_GETTERS.put(type, getters);
199: }
200: return getters;
201: }
202: }
203:
204: /**
205: * Returns {@code true} if the specified method is on the exclusion list.
206: */
207: private static boolean isExcluded(final String name) {
208: for (int i = 0; i < EXCLUDES.length; i++) {
209: if (name.equals(EXCLUDES[i])) {
210: return true;
211: }
212: }
213: return false;
214: }
215:
216: /**
217: * Returns the prefix of the specified method name. If the method name don't starts with
218: * a prefix (for example {@link org.opengis.metadata.quality.ConformanceResult#pass()}),
219: * then this method returns an empty string.
220: */
221: private static String prefix(final String name) {
222: if (name.startsWith(GET)) {
223: return GET;
224: }
225: if (name.startsWith(IS)) {
226: return IS;
227: }
228: if (name.startsWith(SET)) {
229: return SET;
230: }
231: return "";
232: }
233:
234: /**
235: * Returns the number of properties that can be read.
236: */
237: final int count() {
238: return getters.length;
239: }
240:
241: /**
242: * Returns the index of the specified property, or -1 if none.
243: * The search is case-insensitive.
244: */
245: final int indexOf(String key) {
246: key = key.trim();
247: for (int i = 0; i < getters.length; i++) {
248: final String name = getters[i].getName();
249: final int base = prefix(name).length();
250: final int length = key.length();
251: if (name.length() == base + length
252: && name.regionMatches(true, base, key, 0, length)) {
253: return i;
254: }
255: }
256: return -1;
257: }
258:
259: /**
260: * Returns {@code true} if the specified string starting at the specified index contains
261: * no lower case characters. The characters don't have to be in upper case however (e.g.
262: * non-alphabetic characters)
263: */
264: private static boolean isAcronym(final String name, int offset) {
265: final int length = name.length();
266: while (offset < length) {
267: if (Character.isLowerCase(name.charAt(offset++))) {
268: return false;
269: }
270: }
271: return true;
272: }
273:
274: /**
275: * Returns the name of the property at the given index, or {@code null} if none.
276: */
277: final String name(final int index) {
278: if (index >= 0 && index < getters.length) {
279: String name = getters[index].getName();
280: final int base = prefix(name).length();
281: /*
282: * Remove the "get" or "is" prefix and turn the first character after the
283: * prefix into lower case. For example the method name "getTitle" will be
284: * replaced by the property name "title". We will performs this operation
285: * only if there is at least 1 character after the prefix.
286: */
287: if (name.length() > base) {
288: if (isAcronym(name, base)) {
289: name = name.substring(base);
290: } else {
291: final char up = name.charAt(base);
292: final char lo = Character.toLowerCase(up);
293: if (up != lo) {
294: name = lo + name.substring(base + 1);
295: } else {
296: name = name.substring(base);
297: }
298: }
299: }
300: return name;
301: }
302: return null;
303: }
304:
305: /**
306: * Returns the type of the property at the given index.
307: */
308: final Class type(final int index) {
309: if (index >= 0 && index < getters.length) {
310: return getters[index].getReturnType();
311: }
312: return null;
313: }
314:
315: /**
316: * Returns {@code true} if the property at the given index is writable.
317: */
318: final boolean isWritable(final int index) {
319: return (index >= 0) && (index < getters.length)
320: && (setters != null) && (setters[index] != null);
321: }
322:
323: /**
324: * Returns the value for the specified metadata, or {@code null} if none.
325: */
326: final Object get(final int index, final Object metadata) {
327: return (index >= 0 && index < getters.length) ? get(
328: getters[index], metadata) : null;
329: }
330:
331: /**
332: * Gets a value from the specified metadata. We do not expect any checked exception to
333: * be thrown, since {@code org.opengis.metadata} do not declare any.
334: *
335: * @param method The method to use for the query.
336: * @param metadata The metadata object to query.
337: */
338: private static Object get(final Method method, final Object metadata) {
339: assert !method.getReturnType().equals(Void.TYPE) : method;
340: try {
341: return method.invoke(metadata, (Object[]) null);
342: } catch (IllegalAccessException e) {
343: // Should never happen since 'getters' should contains only public methods.
344: throw new AssertionError(e);
345: } catch (InvocationTargetException e) {
346: final Throwable cause = e.getTargetException();
347: if (cause instanceof RuntimeException) {
348: throw (RuntimeException) cause;
349: }
350: if (cause instanceof Error) {
351: throw (Error) cause;
352: }
353: throw new UndeclaredThrowableException(cause);
354: }
355: }
356:
357: /**
358: * Set a value for the specified metadata.
359: *
360: * @return The old value.
361: * @throws IllegalArgumentException if the specified property can't be set.
362: */
363: final Object set(final int index, final Object metadata,
364: final Object value) throws IllegalArgumentException {
365: if (index >= 0 && index < getters.length && setters != null) {
366: final Method setter = setters[index];
367: if (setter != null) {
368: final Object old = get(getters[index], metadata);
369: set(setter, metadata, new Object[] { value });
370: return old;
371: }
372: }
373: throw new IllegalArgumentException(Errors.format(
374: ErrorKeys.ILLEGAL_ARGUMENT_$1, "key"));
375: }
376:
377: /**
378: * Sets a value for the specified metadata. We do not expect any checked exception to
379: * be thrown.
380: *
381: * @param method The method to use for the query.
382: * @param metadata The metadata object to query.
383: */
384: private static void set(final Method method, final Object metadata,
385: final Object[] arguments) {
386: try {
387: method.invoke(metadata, arguments);
388: } catch (IllegalAccessException e) {
389: // Should never happen since 'setters' should contains only public methods.
390: throw new AssertionError(e);
391: } catch (InvocationTargetException e) {
392: final Throwable cause = e.getTargetException();
393: if (cause instanceof RuntimeException) {
394: throw (RuntimeException) cause;
395: }
396: if (cause instanceof Error) {
397: throw (Error) cause;
398: }
399: throw new UndeclaredThrowableException(cause);
400: }
401: }
402:
403: /**
404: * Compares the two specified metadata objects. The comparaison is <cite>shallow</cite>,
405: * i.e. all metadata attributes are compared using the {@link Object#equals} method without
406: * recursive call to this {@code shallowEquals} method for other metadata.
407: * <p>
408: * This method can optionaly excludes null values from the comparaison. In metadata,
409: * null value often means "don't know", so in some occasion we want to consider two
410: * metadata as different only if an attribute value is know for sure to be different.
411: *
412: * @param metadata1 The first metadata object to compare.
413: * @param metadata2 The second metadata object to compare.
414: * @param skipNulls If {@code true}, only non-null values will be compared.
415: */
416: public boolean shallowEquals(final Object metadata1,
417: final Object metadata2, final boolean skipNulls) {
418: assert type.isInstance(metadata1);
419: assert type.isInstance(metadata2);
420: for (int i = 0; i < getters.length; i++) {
421: final Method method = getters[i];
422: final Object value1 = get(method, metadata1);
423: final Object value2 = get(method, metadata2);
424: final boolean empty1 = isEmpty(value1);
425: final boolean empty2 = isEmpty(value2);
426: if (empty1 && empty2) {
427: continue;
428: }
429: if (!Utilities.equals(value1, value2)) {
430: if (!skipNulls || (!empty1 && !empty2)) {
431: return false;
432: }
433: }
434: }
435: return true;
436: }
437:
438: /**
439: * Copies all metadata from source to target. The source can be any implementation of
440: * the metadata interface, but the target must be the implementation expected by this
441: * class.
442: *
443: * @param source The metadata to copy.
444: * @param target The target metadata.
445: * @param skipNulls If {@code true}, only non-null values will be copied.
446: * @return {@code true} in case of success, or {@code false} if at least
447: * one setter method was not found.
448: * @throws UnmodifiableMetadataException if the target metadata is unmodifiable.
449: */
450: public boolean shallowCopy(final Object source,
451: final Object target, final boolean skipNulls)
452: throws UnmodifiableMetadataException {
453: boolean success = true;
454: assert type.isInstance(source);
455: assert implementation.isInstance(target);
456: final Object[] arguments = new Object[1];
457: for (int i = 0; i < getters.length; i++) {
458: arguments[0] = get(getters[i], source);
459: if (!skipNulls || !isEmpty(arguments[0])) {
460: if (setters == null) {
461: return false;
462: }
463: final Method setter = setters[i];
464: if (setter != null) {
465: set(setter, target, arguments);
466: } else {
467: success = false;
468: }
469: }
470: }
471: return success;
472: }
473:
474: /**
475: * Replaces every properties in the specified metadata by their
476: * {@linkplain ModifiableMetadata#unmodifiable unmodifiable variant.
477: */
478: final void freeze(final Object metadata) {
479: assert implementation.isInstance(metadata);
480: if (setters != null) {
481: final Object[] arguments = new Object[1];
482: for (int i = 0; i < getters.length; i++) {
483: final Method setter = setters[i];
484: if (setter != null) {
485: final Object source = get(getters[i], metadata);
486: final Object target = ModifiableMetadata
487: .unmodifiable(source);
488: if (source != target) {
489: arguments[0] = target;
490: set(setter, metadata, arguments);
491: }
492: }
493: }
494: }
495: }
496:
497: /**
498: * Returns {@code true} if the metadata is modifiable. This method is not public because it
499: * uses heuristic rules. In case of doubt, this method conservatively returns {@code true}.
500: */
501: final boolean isModifiable() {
502: if (setters != null) {
503: return true;
504: }
505: for (int i = 0; i < getters.length; i++) {
506: // Immutable objects usually don't need to be cloned. So if
507: // an object is cloneable, it is probably not immutable.
508: if (Cloneable.class.isAssignableFrom(getters[i]
509: .getReturnType())) {
510: return true;
511: }
512: }
513: return false;
514: }
515:
516: /**
517: * Returns a hash code for the specified metadata. The hash code is defined as the
518: * sum of hash code values of all non-null properties. This is the same contract than
519: * {@link java.util.Set#hashCode} and ensure that the hash code value is insensitive
520: * to the ordering of properties.
521: */
522: public int hashCode(final Object metadata) {
523: assert type.isInstance(metadata);
524: int code = 0;
525: for (int i = 0; i < getters.length; i++) {
526: final Object value = get(getters[i], metadata);
527: if (!isEmpty(value)) {
528: code += value.hashCode();
529: }
530: }
531: return code;
532: }
533:
534: /**
535: * Counts the number of non-null properties.
536: */
537: public int count(final Object metadata, final int max) {
538: assert type.isInstance(metadata);
539: int count = 0;
540: for (int i = 0; i < getters.length; i++) {
541: if (!isEmpty(get(getters[i], metadata))) {
542: if (++count >= max) {
543: break;
544: }
545: }
546: }
547: return count;
548: }
549:
550: /**
551: * Returns {@code true} if the specified object is null or an empty collection,
552: * array or string.
553: */
554: static boolean isEmpty(final Object value) {
555: return value == null
556: || ((value instanceof Collection) && ((Collection) value)
557: .isEmpty())
558: || ((value instanceof CharSequence) && value.toString()
559: .trim().length() == 0)
560: || (value.getClass().isArray() && Array
561: .getLength(value) == 0);
562: }
563: }
|