/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2001-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. * * This package contains documentation from OpenGIS specifications. * OpenGIS consortium's work is fully acknowledged here. */ package org.geotools.referencing.operation; import java.text.ParseException; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.Set; import java.util.HashSet; import java.util.TreeSet; import javax.measure.unit.Unit; import javax.measure.quantity.Length; import javax.measure.converter.ConversionException; import org.opengis.metadata.citation.Citation; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.FactoryException; import org.opengis.referencing.NoSuchIdentifierException; import org.opengis.referencing.datum.Ellipsoid; import org.opengis.referencing.cs.CoordinateSystem; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.Conversion; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.MathTransformFactory; import org.opengis.referencing.operation.Matrix; import org.opengis.referencing.operation.Operation; import org.opengis.referencing.operation.OperationMethod; import org.opengis.referencing.operation.Projection; import org.geotools.metadata.iso.citation.Citations; import org.geotools.factory.Hints; import org.geotools.factory.FactoryRegistry; import org.geotools.parameter.Parameters; import org.geotools.parameter.ParameterWriter; import org.geotools.referencing.AbstractIdentifiedObject; import org.geotools.referencing.cs.AbstractCS; import org.geotools.referencing.factory.ReferencingFactory; import org.geotools.referencing.operation.matrix.MatrixFactory; import org.geotools.referencing.operation.transform.ConcatenatedTransform; import org.geotools.referencing.operation.transform.PassThroughTransform; import org.geotools.referencing.operation.transform.ProjectiveTransform; import org.geotools.referencing.wkt.MathTransformParser; import org.geotools.referencing.wkt.Symbols; import org.geotools.resources.Arguments; import org.geotools.resources.LazySet; import org.geotools.resources.CRSUtilities; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.util.CanonicalSet; /** * Low level factory for creating {@linkplain MathTransform math transforms}. * Many high level GIS applications will never need to use this factory directly; * they can use a {@linkplain DefaultCoordinateOperationFactory coordinate operation factory} * instead. However, the {@code MathTransformFactory} interface can be used directly * by applications that wish to transform other types of coordinates (e.g. color coordinates, * or image pixel coordinates). * <P> * A {@linkplain MathTransform math transform} is an object that actually does * the work of applying formulae to coordinate values. The math transform does * not know or care how the coordinates relate to positions in the real world. * This lack of semantics makes implementing {@code MathTransformFactory} * significantly easier than it would be otherwise. * * For example the affine transform applies a matrix to the coordinates * without knowing how what it is doing relates to the real world. So if * the matrix scales <var>Z</var> values by a factor of 1000, then it could * be converting meters into millimeters, or it could be converting kilometers * into meters. * <P> * Because {@linkplain MathTransform math transforms} have low semantic value * (but high mathematical value), programmers who do not have much knowledge * of how GIS applications use coordinate systems, or how those coordinate * systems relate to the real world can implement {@code MathTransformFactory}. * The low semantic content of {@linkplain MathTransform math transforms} also * means that they will be useful in applications that have nothing to do with * GIS coordinates. For example, a math transform could be used to map color * coordinates between different color spaces, such as converting (red, green, blue) * colors into (hue, light, saturation) colors. * <P> * Since a {@linkplain MathTransform math transform} does not know what its source * and target coordinate systems mean, it is not necessary or desirable for a math * transform object to keep information on its source and target coordinate systems. * * @since 2.1 * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) * * @tutorial http://docs.codehaus.org/display/GEOTOOLS/Coordinate+Transformation+Parameters */ public class DefaultMathTransformFactory extends ReferencingFactory implements MathTransformFactory { /** * The hints to provide to math transform providers. Null for now, but may be non-null * in some future version. */ private static final Hints HINTS = null; /** * The object to use for parsing <cite>Well-Known Text</cite> (WKT) strings. * Will be created only when first needed. */ private transient MathTransformParser parser; /** * The last value returned by {@link #getProvider}. Stored as an * optimization since the same provider is often asked many times. */ private transient MathTransformProvider lastProvider; /** * The operation method for the last transform created. */ private static final ThreadLocal<OperationMethod> lastMethod = new ThreadLocal<OperationMethod>(); /** * A pool of math transform. This pool is used in order to * returns instance of existing math transforms when possible. */ private final CanonicalSet<MathTransform> pool; /** * The service registry for finding {@link MathTransformProvider} implementations. */ private final FactoryRegistry registry; /** * Constructs a default {@link MathTransform math transform} factory. */ public DefaultMathTransformFactory() { this(new Class<?>[] {MathTransformProvider.class}); } /** * Constructs a default {@link MathTransform math transform} factory using the * specified {@linkplain MathTransformProvider transform providers} categories. * * @param categories The providers categories, as implementations * of {@link MathTransformProvider}. */ private DefaultMathTransformFactory(final Class<?>[] categories) { registry = new FactoryRegistry(Arrays.asList(categories)); pool = CanonicalSet.newInstance(MathTransform.class); } /** * Returns the vendor responsible for creating this factory implementation. Many implementations * may be available for the same factory interface. The default implementation returns * {@linkplain Citations#GEOTOOLS Geotools}. * * @return The vendor for this factory implementation. */ @Override public Citation getVendor() { return Citations.GEOTOOLS; } /** * Returns a set of available methods for {@linkplain MathTransform math transforms}. For * each element in this set, the {@linkplain OperationMethod#getName operation method name} * must be known to the {@link #getDefaultParameters} method in this factory. * The set of available methods is implementation dependent. * * @param type <code>{@linkplain Operation}.class</code> for fetching all operation methods, * or <code>{@linkplain Projection}.class</code> for fetching only map projection * methods. * @return All {@linkplain MathTransform math transform} methods available in this factory. * * @see #getDefaultParameters * @see #createParameterizedTransform */ public Set<OperationMethod> getAvailableMethods(final Class<? extends Operation> type) { return new LazySet<OperationMethod>(registry.getServiceProviders(MathTransformProvider.class, (type!=null) ? new MethodFilter(type) : null, HINTS)); } /** * A filter for the set of available operations. */ private static final class MethodFilter implements FactoryRegistry.Filter { /** * The expected type ({@code Projection.class}) for projections). */ private final Class<? extends Operation> type; /** * Constructs a filter for the set of math operations methods. */ public MethodFilter(final Class<? extends Operation> type) { this.type = type; } /** * Returns {@code true} if the specified element should be included. If the type is * unknown, conservatively returns {@code true}. */ public boolean filter(final Object element) { if (element instanceof MathTransformProvider) { final Class<? extends Operation> t = ((MathTransformProvider) element).getOperationType(); if (t!=null && !type.isAssignableFrom(t)) { return false; } } return true; } } /** * Returns the operation method used for the latest call to * {@link #createParameterizedTransform createParameterizedTransform} * in the currently running thread. Returns {@code null} if not applicable. * * @see #createParameterizedTransform * * @since 2.5 */ public OperationMethod getLastMethodUsed() { return lastMethod.get(); } /** * Returns the operation method for the specified name. * * @param name The case insensitive * {@linkplain org.opengis.metadata.Identifier#getCode identifier code} * of the operation method to search for (e.g. {@code "Transverse_Mercator"}). * @return The operation method. * @throws NoSuchIdentifierException if there is no operation method registered for the * specified name. * * @since 2.2 */ public OperationMethod getOperationMethod(String name) throws NoSuchIdentifierException { return getProvider(name); } /** * Returns the math transform provider for the specified operation method. * This provider can be used in order to query parameter for a method name * (e.g. <code>getProvider("Transverse_Mercator").getParameters()</code>), * or any of the alias in a given locale. * * @param method The case insensitive * {@linkplain org.opengis.metadata.Identifier#getCode identifier code} * of the operation method to search for (e.g. {@code "Transverse_Mercator"}). * @return The math transform provider. * @throws NoSuchIdentifierException if there is no provider registered for the specified * method. */ private MathTransformProvider getProvider(final String method) throws NoSuchIdentifierException { /* * Copies the 'lastProvider' reference in order to avoid synchronization. This is safe * because copy of object references are atomic operations. Note that this is not the * deprecated "double check" idiom since we are not creating new objects, but checking * for existing ones. */ MathTransformProvider provider = lastProvider; if (provider!=null && provider.nameMatches(method)) { return provider; } final Iterator<MathTransformProvider> providers = registry.getServiceProviders(MathTransformProvider.class, null, HINTS); while (providers.hasNext()) { provider = providers.next(); if (provider.nameMatches(method)) { return lastProvider = provider; } } throw new NoSuchIdentifierException(Errors.format( ErrorKeys.NO_TRANSFORM_FOR_CLASSIFICATION_$1, method), method); } /** * Returns the default parameter values for a math transform using the given method. * The method argument is the name of any operation method returned by the * {@link #getAvailableMethods} method. A typical example is * <code>"<A HREF="http://www.remotesensing.org/geotiff/proj_list/transverse_mercator.html">Transverse_Mercator</A>"</code>). * * <P>This method creates new parameter instances at every call. It is intented to be modified * by the user before to be passed to <code>{@linkplain #createParameterizedTransform * createParameterizedTransform}(parameters)</code>.</P> * * @param method The case insensitive name of the method to search for. * @return The default parameter values. * @throws NoSuchIdentifierException if there is no transform registered for the specified * method. * * @see #getAvailableMethods * @see #createParameterizedTransform * @see org.geotools.referencing.operation.transform.AbstractMathTransform#getParameterValues */ public ParameterValueGroup getDefaultParameters(final String method) throws NoSuchIdentifierException { return getProvider(method).getParameters().createValue(); } /** * Creates a {@linkplain #createParameterizedTransform parameterized transform} from a base * CRS to a derived CS. If the {@code "semi_major"} and {@code "semi_minor"} parameters are * not explicitly specified, they will be inferred from the {@linkplain Ellipsoid ellipsoid} * and added to {@code parameters}. In addition, this method performs axis switch as needed. * <p> * The {@linkplain OperationMethod operation method} used can be obtained by a call to * {@link #getLastUsedMethod}. * * @param baseCRS The source coordinate reference system. * @param parameters The parameter values for the transform. * @param derivedCS the target coordinate system. * @return The parameterized transform. * @throws NoSuchIdentifierException if there is no transform registered for the method. * @throws FactoryException if the object creation failed. This exception is thrown * if some required parameter has not been supplied, or has illegal value. */ public MathTransform createBaseToDerived(final CoordinateReferenceSystem baseCRS, final ParameterValueGroup parameters, final CoordinateSystem derivedCS) throws NoSuchIdentifierException, FactoryException { /* * If the user's parameter do not contains semi-major and semi-minor axis length, infers * them from the ellipsoid. This is a convenience service since the user often omit those * parameters (because they duplicate datum information). */ final Ellipsoid ellipsoid = CRSUtilities.getHeadGeoEllipsoid(baseCRS); if (ellipsoid != null) { final Unit<Length> axisUnit = ellipsoid.getAxisUnit(); Parameters.ensureSet(parameters, "semi_major", ellipsoid.getSemiMajorAxis(), axisUnit, false); Parameters.ensureSet(parameters, "semi_minor", ellipsoid.getSemiMinorAxis(), axisUnit, false); } MathTransform baseToDerived = createParameterizedTransform(parameters); final OperationMethod method = lastMethod.get(); baseToDerived = createBaseToDerived(baseCRS, baseToDerived, derivedCS); lastMethod.set(method); return baseToDerived; } /** * Creates a transform from a base CRS to a derived CS. This method expects a "raw" * transform without unit conversion or axis switch, typically a map projection * working on (<cite>longitude</cite>, <cite>latitude</cite>) axes in degrees and * (<cite>x</cite>, <cite>y</cite>) axes in metres. This method inspects the coordinate * systems and prepend or append the unit conversions and axis switchs automatically. * * @param baseCRS The source coordinate reference system. * @param projection The "raw" <cite>base to derived</cite> transform. * @param derivedCS the target coordinate system. * @return The parameterized transform. * @throws FactoryException if the object creation failed. This exception is thrown * if some required parameter has not been supplied, or has illegal value. * * @since 2.5 */ public MathTransform createBaseToDerived(final CoordinateReferenceSystem baseCRS, final MathTransform projection, final CoordinateSystem derivedCS) throws FactoryException { /* * Computes matrix for swapping axis and performing units conversion. * There is one matrix to apply before projection on (longitude,latitude) * coordinates, and one matrix to apply after projection on (easting,northing) * coordinates. */ final CoordinateSystem sourceCS = baseCRS.getCoordinateSystem(); final Matrix swap1, swap3; try { swap1 = AbstractCS.swapAndScaleAxis(sourceCS, AbstractCS.standard(sourceCS)); swap3 = AbstractCS.swapAndScaleAxis(AbstractCS.standard(derivedCS), derivedCS); } catch (IllegalArgumentException cause) { // User-specified axis don't match. throw new FactoryException(cause); } catch (ConversionException cause) { // A Unit conversion is non-linear. throw new FactoryException(cause); } /* * Prepares the concatenation of the matrix computed above and the projection. * Note that at this stage, the dimensions between each step may not be compatible. * For example the projection (step2) is usually two-dimensional while the source * coordinate system (step1) may be three-dimensional if it has a height. */ MathTransform step1 = createAffineTransform(swap1); MathTransform step3 = createAffineTransform(swap3); MathTransform step2 = projection; /* * If the target coordinate system has a height, instructs the projection to pass * the height unchanged from the base CRS to the target CRS. After this block, the * dimensions of 'step2' and 'step3' should match. */ final int numTrailingOrdinates = step3.getSourceDimensions() - step2.getTargetDimensions(); if (numTrailingOrdinates > 0) { step2 = createPassThroughTransform(0, step2, numTrailingOrdinates); } /* * If the source CS has a height but the target CS doesn't, drops the extra coordinates. * After this block, the dimensions of 'step1' and 'step2' should match. */ final int sourceDim = step1.getTargetDimensions(); final int targetDim = step2.getSourceDimensions(); if (sourceDim > targetDim) { final Matrix drop = MatrixFactory.create(targetDim+1, sourceDim+1); drop.setElement(targetDim, sourceDim, 1); step1 = createConcatenatedTransform(createAffineTransform(drop), step1); } return createConcatenatedTransform(createConcatenatedTransform(step1, step2), step3); } /** * Creates a transform from a group of parameters. The method name is inferred from the * {@linkplain org.opengis.parameter.ParameterDescriptorGroup#getName parameter group name}. * Example: * * <blockquote><pre> * ParameterValueGroup p = factory.getDefaultParameters("Transverse_Mercator"); * p.parameter("semi_major").setValue(6378137.000); * p.parameter("semi_minor").setValue(6356752.314); * MathTransform mt = factory.createParameterizedTransform(p); * </pre></blockquote> * * @param parameters The parameter values. * @return The parameterized transform. * @throws NoSuchIdentifierException if there is no transform registered for the method. * @throws FactoryException if the object creation failed. This exception is thrown * if some required parameter has not been supplied, or has illegal value. * * @see #getDefaultParameters * @see #getAvailableMethods * @see #getLastUsedMethod */ public MathTransform createParameterizedTransform(ParameterValueGroup parameters) throws NoSuchIdentifierException, FactoryException { MathTransform transform; OperationMethod method = null; try { final String classification = parameters.getDescriptor().getName().getCode(); final MathTransformProvider provider = getProvider(classification); method = provider; try { parameters = provider.ensureValidValues(parameters); transform = provider.createMathTransform(parameters); } catch (IllegalArgumentException exception) { /* * Catch only exceptions which may be the result of improper parameter * usage (e.g. a value out of range). Do not catch exception caused by * programming errors (e.g. null pointer exception). */ throw new FactoryException(exception); } if (transform instanceof MathTransformProvider.Delegate) { final MathTransformProvider.Delegate delegate = (MathTransformProvider.Delegate) transform; method = delegate.method; transform = delegate.transform; } transform = pool.unique(transform); } finally { lastMethod.set(method); // May be null in case of failure, which is intented. } return transform; } /** * Creates an affine transform from a matrix. * If the transform's input dimension is {@code M}, and output dimension * is {@code N}, then the matrix will have size {@code [N+1][M+1]}. * The +1 in the matrix dimensions allows the matrix to do a shift, as well as * a rotation. The {@code [M][j]} element of the matrix will be the j'th * ordinate of the moved origin. The {@code [i][N]} element of the matrix * will be 0 for <var>i</var> less than {@code M}, and 1 for <var>i</var> * equals {@code M}. * * @param matrix The matrix used to define the affine transform. * @return The affine transform. * @throws FactoryException if the object creation failed. */ public MathTransform createAffineTransform(final Matrix matrix) throws FactoryException { lastMethod.remove(); // To be strict, we should set ProjectiveTransform.Provider return pool.unique(ProjectiveTransform.create(matrix)); } /** * Creates a transform by concatenating two existing transforms. * A concatenated transform acts in the same way as applying two * transforms, one after the other. * * The dimension of the output space of the first transform must match * the dimension of the input space in the second transform. * If you wish to concatenate more than two transforms, then you can * repeatedly use this method. * * @param transform1 The first transform to apply to points. * @param transform2 The second transform to apply to points. * @return The concatenated transform. * @throws FactoryException if the object creation failed. */ public MathTransform createConcatenatedTransform(final MathTransform transform1, final MathTransform transform2) throws FactoryException { MathTransform tr; try { tr = ConcatenatedTransform.create(transform1, transform2); } catch (IllegalArgumentException exception) { throw new FactoryException(exception); } tr = pool.unique(tr); return tr; } /** * Creates a transform which passes through a subset of ordinates to another transform. * This allows transforms to operate on a subset of ordinates. For example, if you have * (<var>Lat</var>,<var>Lon</var>,<var>Height</var>) coordinates, then you may wish to * convert the height values from meters to feet without affecting the * (<var>Lat</var>,<var>Lon</var>) values. * * @param firstAffectedOrdinate The lowest index of the affected ordinates. * @param subTransform Transform to use for affected ordinates. * @param numTrailingOrdinates Number of trailing ordinates to pass through. * Affected ordinates will range from {@code firstAffectedOrdinate} * inclusive to {@code dimTarget-numTrailingOrdinates} exclusive. * @return A pass through transform with the following dimensions:<br> * <pre> * Source: firstAffectedOrdinate + subTransform.getSourceDimensions() + numTrailingOrdinates * Target: firstAffectedOrdinate + subTransform.getTargetDimensions() + numTrailingOrdinates</pre> * @throws FactoryException if the object creation failed. */ public MathTransform createPassThroughTransform(final int firstAffectedOrdinate, final MathTransform subTransform, final int numTrailingOrdinates) throws FactoryException { MathTransform tr; try { tr = PassThroughTransform.create(firstAffectedOrdinate, subTransform, numTrailingOrdinates); } catch (IllegalArgumentException exception) { throw new FactoryException(exception); } tr = pool.unique(tr); return tr; } /** * Creates a math transform object from a XML string. The default implementation * always throws an exception, since this method is not yet implemented. * * @param xml Math transform encoded in XML format. * @throws FactoryException if the object creation failed. */ public MathTransform createFromXML(String xml) throws FactoryException { throw new FactoryException("Not yet implemented."); } /** * Creates a math transform object from a * <A HREF="http://geoapi.sourceforge.net/snapshot/javadoc/org/opengis/referencing/doc-files/WKT.html"><cite>Well * Known Text</cite> (WKT)</A>. * * @param text Math transform encoded in Well-Known Text format. * @return The math transform (never {@code null}). * @throws FactoryException if the Well-Known Text can't be parsed, * or if the math transform creation failed from some other reason. */ public synchronized MathTransform createFromWKT(final String text) throws FactoryException { // Note: while this factory is thread safe, the WKT parser is not. // Since we share a single instance of this parser, we must // synchronize. if (parser == null) { parser = new MathTransformParser(Symbols.DEFAULT, this); } try { return parser.parseMathTransform(text); } catch (ParseException exception) { final Throwable cause = exception.getCause(); if (cause instanceof FactoryException) { throw (FactoryException) cause; } throw new FactoryException(exception); } } /** * Scans for factory plug-ins on the application class path. This method is * needed because the application class path can theoretically change, or * additional plug-ins may become available. Rather than re-scanning the * classpath on every invocation of the API, the class path is scanned * automatically only on the first invocation. Clients can call this * method to prompt a re-scan. Thus this method need only be invoked by * sophisticated applications which dynamically make new plug-ins * available at runtime. */ public void scanForPlugins() { registry.scanForPlugins(); } /** * Dump to the standard output stream a list of available operation methods. * This method can be invoked from the command line. It provides a mean to * verify which transforms were found in the classpath. The syntax is: * <BR> * <BLOCKQUOTE><CODE> * java org.geotools.referencing.operation.DefaultMathTransformFactory * <VAR><options></VAR> <VAR><method></VAR> * </CODE></BLOCKQUOTE> * * <P>where options are:</P> * * <TABLE CELLPADDING='0' CELLSPACING='0'> * <TR><TD NOWRAP><CODE>-projections</CODE></TD> * <TD NOWRAP> List only projections</TD></TR> * <TR><TD NOWRAP><CODE>-conversions</CODE></TD> * <TD NOWRAP> List only conversions</TD></TR> * <TR><TD NOWRAP><CODE>-all</CODE></TD> * <TD NOWRAP> List the parameters for all transforms</TD></TR> * <TR><TD NOWRAP><CODE>-encoding</CODE> <VAR><code></VAR></TD> * <TD NOWRAP> Set the character encoding</TD></TR> * <TR><TD NOWRAP><CODE>-locale</CODE> <VAR><language></VAR></TD> * <TD NOWRAP> Set the language for the output (e.g. "fr" for French)</TD></TR> * </TABLE> * * <P>and <VAR><method></VAR> is the optional name of an operation method * (e.g. <CODE>"Affine"</CODE>, <CODE>"EPSG:9624"</CODE> or just * <CODE>"9624"</CODE> for the affine transform method).</P> * * <P><strong>Note for Windows users:</strong> If the output contains strange * symbols, try to supply an "{@code -encoding}" argument. Example:</P> * * <blockquote><code> * java org.geotools.referencing.operation.DefaultMathTransformFactory -encoding Cp850 * </code></blockquote> * * <P>The codepage number (850 in the previous example) can be obtained from the DOS * commande line using the "{@code chcp}" command with no arguments.</P> * * @param args Command line arguments. */ public static void main(String[] args) { /* * Parse the command-line arguments and print the summary. */ final Arguments arguments = new Arguments(args); final boolean printAll = arguments.getFlag("-all"); Class<? extends Operation> type = null; if (arguments.getFlag("-projections")) type = Projection.class; if (arguments.getFlag("-conversions")) type = Conversion.class; args = arguments.getRemainingArguments(1); try { final DefaultMathTransformFactory factory = new DefaultMathTransformFactory(); final ParameterWriter writer = new ParameterWriter(arguments.out); writer.setLocale(arguments.locale); Set<OperationMethod> methods = Collections.emptySet(); if (printAll || args.length==0) { final Set<String> scopes = new HashSet<String>(); // scopes.add("OGC"); // Omitted because usually the same than 'identifier'. scopes.add("EPSG"); scopes.add("Geotools"); // Limit the number of columns to output. methods = new TreeSet<OperationMethod>(AbstractIdentifiedObject.NAME_COMPARATOR); methods.addAll(factory.getAvailableMethods(type)); writer.summary(methods, scopes); } if (!printAll) { if (args.length == 0) { methods = Collections.emptySet(); } else { methods = Collections.singleton((OperationMethod) factory.getProvider(args[0])); } } /* * Iterates through all math transform to print. It may be a singleton * if the user ask for a specific math transform. */ final String lineSeparator = System.getProperty("line.separator", "\n"); for (final OperationMethod method : methods) { arguments.out.write(lineSeparator); writer.format(method); } } catch (NoSuchIdentifierException exception) { arguments.err.println(exception.getLocalizedMessage()); return; } catch (Exception exception) { exception.printStackTrace(arguments.err); return; } arguments.out.flush(); } /** * Cleans up the thread local set in this thread. They can prevent web applications from * proper shutdown */ public static void cleanupThreadLocals() { lastMethod.remove(); } }