diff --git a/lucene/contrib/CHANGES.txt b/lucene/contrib/CHANGES.txt index e1986441b23..71147904e7b 100644 --- a/lucene/contrib/CHANGES.txt +++ b/lucene/contrib/CHANGES.txt @@ -66,7 +66,11 @@ Bug fixes not an indexed field. Note, this change affects the APIs. (Grant Ingersoll) * LUCENE-2359: Fix bug in CartesianPolyFilterBuilder related to handling of behavior around - the 180th meridian (Grant Ingersoll) + the 180th meridian (Grant Ingersoll, Nicolas Hellinger) + + * LUCENE-2366: Fix LLRect issue with properly calculating box points (Nicolas Hellinger via Grant Ingersoll) + + API Changes diff --git a/lucene/contrib/spatial/src/java/org/apache/lucene/spatial/geometry/LatLng.java b/lucene/contrib/spatial/src/java/org/apache/lucene/spatial/geometry/LatLng.java index b2128ab207a..1bcfd280763 100644 --- a/lucene/contrib/spatial/src/java/org/apache/lucene/spatial/geometry/LatLng.java +++ b/lucene/contrib/spatial/src/java/org/apache/lucene/spatial/geometry/LatLng.java @@ -30,6 +30,19 @@ package org.apache.lucene.spatial.geometry; */ public abstract class LatLng { + public final static int LONGITUDE_DEGREE_RANGE = 360; + public final static int LONGITUDE_DEGREE_MIN = -LONGITUDE_DEGREE_RANGE / 2; + public final static int LONGITUDE_DEGREE_MAX = LONGITUDE_DEGREE_RANGE / 2; + public final static int LATITUDE_DEGREE_RANGE = 180; + public final static int LATITUDE_DEGREE_MIN = -LATITUDE_DEGREE_RANGE / 2; + public final static int LATITUDE_DEGREE_MAX = LATITUDE_DEGREE_RANGE / 2; + + public final static int HEADING_NORTH= 0; + public final static int HEADING_SOUTH= 180; + public final static int HEADING_EAST= 90; + public final static int HEADING_WEST= 270; + + public abstract boolean isNormalized(); public abstract boolean isFixedPoint(); @@ -69,6 +82,47 @@ public abstract class LatLng { ); } + public static LatLng computeDestination(LatLng startPoint, double distance, + double heading) { + return computeDestination(startPoint, distance, heading, + DistanceUnits.MILES); + } + + /** + * Utility method for calculating a target point given a start, a heading (in degrees), a distance and a distance unit + * @param startPoint + * beginning point of the path + * @param distance + * distance to be travel from the starting point + * @param heading + * heading in degrees to follow during the travel + * @param distanceUnit + * unit of the distance to to travel + * @return arrival point of the described travel + */ + public static LatLng computeDestination(LatLng startPoint, double distance, double heading, DistanceUnits distanceUnit) { + double startPointLatitude = startPoint.getLat(); + double startPointLongitude = startPoint.getLng(); + double earthRadius = distanceUnit.earthRadius(); + double headingRad = Math.toRadians(heading); + double startPointLatitudeRad = Math.toRadians(startPointLatitude); + double startPointLongitudeRad = Math.toRadians(startPointLongitude); + + // Haversine formula (http://www.movable-type.co.uk/scripts/latlong.html) + double destinationLatitudeRad = Math.asin(Math.sin(startPointLatitudeRad) + * Math.cos(distance / earthRadius) + Math.cos(startPointLatitudeRad) + * Math.sin(distance / earthRadius) * Math.cos(headingRad)); + + double destinationLongitudeRad = startPointLongitudeRad + + Math.atan2(Math.sin(headingRad) * Math.sin(distance / earthRadius) + * Math.cos(startPointLatitudeRad), Math.cos(distance / earthRadius) + - Math.sin(startPointLatitudeRad) + * Math.sin(destinationLatitudeRad)); + + return new FloatLatLng(Math.toDegrees(destinationLatitudeRad), Math + .toDegrees(destinationLongitudeRad)); + } + /** * The inverse of toCartesian(). Always returns a FixedLatLng. * @param pt diff --git a/lucene/contrib/spatial/src/java/org/apache/lucene/spatial/geometry/shape/LLRect.java b/lucene/contrib/spatial/src/java/org/apache/lucene/spatial/geometry/shape/LLRect.java index 9ff8c7ee434..b8c63b6a66b 100644 --- a/lucene/contrib/spatial/src/java/org/apache/lucene/spatial/geometry/shape/LLRect.java +++ b/lucene/contrib/spatial/src/java/org/apache/lucene/spatial/geometry/shape/LLRect.java @@ -17,6 +17,7 @@ package org.apache.lucene.spatial.geometry.shape; +import org.apache.lucene.spatial.geometry.DistanceUnits; import org.apache.lucene.spatial.geometry.FloatLatLng; import org.apache.lucene.spatial.geometry.LatLng; @@ -74,14 +75,34 @@ public class LLRect { * @param heightMi */ public static LLRect createBox(LatLng center, double widthMi, double heightMi) { - double d = widthMi; - LatLng ur = boxCorners(center, d, 45.0); // assume right angles - LatLng ll = boxCorners(center, d, 225.0); + double minLat; + double maxLat; + double minLng; + double maxLng; + double radius= Math.max(widthMi, heightMi); + + if (radius > center.arcDistance(new FloatLatLng(LatLng.LATITUDE_DEGREE_MAX, LatLng.HEADING_NORTH))) { + maxLat = LatLng.LATITUDE_DEGREE_MAX; + } else { + maxLat = LatLng.computeDestination(center, radius, LatLng.HEADING_NORTH).getLat(); + + } + if (radius > center.arcDistance(new FloatLatLng(LatLng.LATITUDE_DEGREE_MIN, LatLng.HEADING_NORTH))) { + minLat = LatLng.LATITUDE_DEGREE_MIN; + } else { + minLat = LatLng.computeDestination(center, radius, LatLng.HEADING_SOUTH).getLat(); + } - //System.err.println("boxCorners: ur " + ur.getLat() + ',' + ur.getLng()); - //System.err.println("boxCorners: cnt " + center.getLat() + ',' + center.getLng()); - //System.err.println("boxCorners: ll " + ll.getLat() + ',' + ll.getLng()); - return new LLRect(ll, ur); + if((radius > 2 * Math.PI * DistanceUnits.MILES.earthRadius() * Math.cos(Math.toRadians(minLat))) || + (radius > 2 * Math.PI * DistanceUnits.MILES.earthRadius() * Math.cos(Math.toRadians(maxLat)))) { + maxLng = LatLng.LONGITUDE_DEGREE_MAX; + minLng = LatLng.LONGITUDE_DEGREE_MIN; + } else { + maxLng = LatLng.computeDestination(new FloatLatLng(Math.max(Math.abs(minLat), Math.abs(maxLat)), center.getLng()), radius, LatLng.HEADING_EAST).getLng(); + minLng = LatLng.computeDestination(new FloatLatLng(Math.max(Math.abs(minLat), Math.abs(maxLat)), center.getLng()), radius, LatLng.HEADING_WEST).getLng(); + } + + return new LLRect((new FloatLatLng(minLat, minLng).normalize()), (new FloatLatLng(maxLat, maxLng)).normalize()); } /** diff --git a/lucene/contrib/spatial/src/java/org/apache/lucene/spatial/tier/CartesianPolyFilterBuilder.java b/lucene/contrib/spatial/src/java/org/apache/lucene/spatial/tier/CartesianPolyFilterBuilder.java index d2fa6d9eb73..359d09ef172 100644 --- a/lucene/contrib/spatial/src/java/org/apache/lucene/spatial/tier/CartesianPolyFilterBuilder.java +++ b/lucene/contrib/spatial/src/java/org/apache/lucene/spatial/tier/CartesianPolyFilterBuilder.java @@ -17,9 +17,6 @@ package org.apache.lucene.spatial.tier; -import java.math.BigDecimal; -import java.math.RoundingMode; - import org.apache.lucene.search.Filter; import org.apache.lucene.spatial.tier.projections.CartesianTierPlotter; import org.apache.lucene.spatial.tier.projections.IProjector; @@ -64,121 +61,71 @@ public class CartesianPolyFilterBuilder { miles = MILES_FLOOR; } LLRect box1 = LLRect.createBox( new FloatLatLng( latitude, longitude ), miles, miles ); - LatLng ll = box1.getLowerLeft(); - LatLng ur = box1.getUpperRight(); + LatLng lowerLeft = box1.getLowerLeft(); + LatLng upperRight = box1.getUpperRight(); - double latY = ur.getLat(); - double latX = ll.getLat(); - double longY = ur.getLng(); - double longX = ll.getLng(); - double longX2 = 0.0; - //These two if checks setup us up to deal with issues around the prime meridian and the 180th meridian - //In these two cases, we need to get tiles (tiers) from the lower left up to the meridian and then - //from the meridan to the upper right - //Are we crossing the 180 deg. longitude, if so, we need to do some special things - if (ur.getLng() < 0.0 && ll.getLng() > 0.0) { - longX2 = ll.getLng(); - longX = -180.0; - } - //are we crossing the prime meridian (0 degrees)? If so, we need to account for it and boxes on both sides - if (ur.getLng() > 0.0 && ll.getLng() < 0.0) { - longX2 = ll.getLng(); - longX = 0.0; - } - - //System.err.println("getBoxShape:"+latY+"," + longY); - //System.err.println("getBoxShape:"+latX+"," + longX); - CartesianTierPlotter ctp = new CartesianTierPlotter(2, projector,tierPrefix); - int bestFit = ctp.bestFit(miles); - if (bestFit < minTier){ - bestFit = minTier; - } else if (bestFit > maxTier){ - bestFit = maxTier; - } - - ctp = new CartesianTierPlotter(bestFit, projector,tierPrefix); - Shape shape = new Shape(ctp.getTierFieldName()); - - // generate shape - // iterate from startX->endX - // iterate from startY -> endY - // shape.add(currentLat.currentLong); - //for the edge cases (prime meridian and the 180th meridian), this call handles all tiles East of the meridian - //for all other cases, it handles the whole set of tiles - shape = getShapeLoop(shape,ctp,latX,longX,latY,longY); - if (longX2 != 0.0) { - if (longX == 0.0) { - longX = longX2; - longY = 0.0; - //handles the lower left longitude to the prime meridian - //shape = getShapeLoop(shape, ctp, latX, longX, latY, longY); - } else { - //this clause handles the lower left longitude up to the 180 meridian - longX = longX2; - longY = 180.0; - } - shape = getShapeLoop(shape, ctp, latX, longX, latY, longY); + double latUpperRight = upperRight.getLat(); + double latLowerLeft = lowerLeft.getLat(); + double longUpperRight = upperRight.getLng(); + double longLowerLeft = lowerLeft.getLng(); - //System.err.println("getBoxShape2:"+latY+"," + longY); - //System.err.println("getBoxShape2:"+latX+"," + longX); - } - + CartesianTierPlotter ctp = new CartesianTierPlotter( miles, projector, tierPrefix ); + Shape shape = new Shape(ctp.getTierLevelId()); + + if (longUpperRight < longLowerLeft) { // Box cross the 180 meridian + addBoxes(shape, ctp, latLowerLeft, longLowerLeft, latUpperRight, LatLng.LONGITUDE_DEGREE_MAX); + addBoxes(shape, ctp, latLowerLeft, -LatLng.LONGITUDE_DEGREE_MIN, latUpperRight, longUpperRight); + } else { + addBoxes(shape, ctp, latLowerLeft, longLowerLeft, latUpperRight, longUpperRight); + } return shape; } - public Shape getShapeLoop(Shape shape, CartesianTierPlotter ctp, double latX, double longX, double latY, double longY) - { - - //System.err.println("getShapeLoop:"+latY+"," + longY); - //System.err.println("getShapeLoop:"+latX+"," + longX); - double beginAt = ctp.getTierBoxId(latX, longX); - double endAt = ctp.getTierBoxId(latY, longY); - if (beginAt > endAt){ - double tmp = beginAt; - beginAt = endAt; - endAt = tmp; - } - double tierVert = ctp.getTierVerticalPosDivider(); - //System.err.println(" | "+ beginAt+" | "+ endAt); - - double startX = beginAt - (beginAt %1); - double startY = beginAt - startX ; //should give a whole number - - double endX = endAt - (endAt %1); - double endY = endAt -endX; //should give a whole number - - int scale = (int)Math.log10(tierVert); - endY = new BigDecimal(endY).setScale(scale, RoundingMode.HALF_EVEN).doubleValue(); - startY = new BigDecimal(startY).setScale(scale, RoundingMode.HALF_EVEN).doubleValue(); - double xInc = 1.0d / tierVert; - xInc = new BigDecimal(xInc).setScale(scale, RoundingMode.HALF_EVEN).doubleValue(); - - //System.err.println("go from startX:"+startX+" to:" + endX); - for (; startX <= endX; startX++){ - - double itY = startY; - //System.err.println("go from startY:"+startY+" to:" + endY); - while (itY <= endY){ - //create a boxId - // startX.startY - double boxId = startX + itY ; + private void addBoxes(Shape shape, CartesianTierPlotter tierPlotter, double lat1, double long1, double lat2, double long2) { + double boxId1 = tierPlotter.getTierBoxId(lat1, long1); + double boxId2 = tierPlotter.getTierBoxId(lat2, long2); + + double tierVert = tierPlotter.getTierVerticalPosDivider(); + + int LongIndex1 = (int) Math.round(boxId1); + int LatIndex1 = (int) Math.round((boxId1 - LongIndex1) * tierVert); + + int LongIndex2 = (int) Math.round(boxId2); + int LatIndex2 = (int) Math.round((boxId2 - LongIndex2) * tierVert); + + int startLong, endLong; + int startLat, endLat; + + if (LongIndex1 > LongIndex2) { + startLong = LongIndex2; + endLong = LongIndex1; + } else { + startLong = LongIndex1; + endLong = LongIndex2; + } + + if (LatIndex1 > LatIndex2) { + startLat = LatIndex2; + endLat = LatIndex1; + } else { + startLat = LatIndex1; + endLat = LatIndex2; + } + + int LatIndex, LongIndex; + for (LongIndex = startLong; LongIndex <= endLong; LongIndex++) { + for (LatIndex = startLat; LatIndex <= endLat; LatIndex++) { + // create a boxId + double boxId = LongIndex + LatIndex / tierVert; shape.addBox(boxId); - //System.err.println("----"+startX+" and "+itY); - //System.err.println("----"+boxId); - itY += xInc; - - // java keeps 0.0001 as 1.0E-1 - // which ends up as 0.00011111 - itY = new BigDecimal(itY).setScale(scale, RoundingMode.HALF_EVEN).doubleValue(); } } - return shape; } public Filter getBoundingArea(double latitude, double longitude, double miles) { Shape shape = getBoxShape(latitude, longitude, miles); - return new CartesianShapeFilter(shape, shape.getTierId()); + return new CartesianShapeFilter(shape, tierPrefix + shape.getTierId()); } } diff --git a/lucene/contrib/spatial/src/java/org/apache/lucene/spatial/tier/Shape.java b/lucene/contrib/spatial/src/java/org/apache/lucene/spatial/tier/Shape.java index 2a5bdbbf087..9f9bea4a344 100644 --- a/lucene/contrib/spatial/src/java/org/apache/lucene/spatial/tier/Shape.java +++ b/lucene/contrib/spatial/src/java/org/apache/lucene/spatial/tier/Shape.java @@ -29,9 +29,9 @@ import java.util.List; public class Shape implements Serializable{ private List area = new ArrayList(); - private String tierId; + private int tierId; - public Shape (String tierId){ + public Shape (int tierId){ this.tierId = tierId; } @@ -43,7 +43,7 @@ public class Shape implements Serializable{ return area; } - public String getTierId(){ + public int getTierId(){ return tierId; } diff --git a/lucene/contrib/spatial/src/java/org/apache/lucene/spatial/tier/projections/CartesianTierPlotter.java b/lucene/contrib/spatial/src/java/org/apache/lucene/spatial/tier/projections/CartesianTierPlotter.java index 5e3b48f8696..7da260cf1a1 100644 --- a/lucene/contrib/spatial/src/java/org/apache/lucene/spatial/tier/projections/CartesianTierPlotter.java +++ b/lucene/contrib/spatial/src/java/org/apache/lucene/spatial/tier/projections/CartesianTierPlotter.java @@ -17,6 +17,8 @@ package org.apache.lucene.spatial.tier.projections; +import org.apache.lucene.spatial.geometry.DistanceUnits; + /** *

NOTE: This API is still in * flux and might change in incompatible ways in the next @@ -33,6 +35,8 @@ public class CartesianTierPlotter { final String fieldPrefix; Double idd = Double.valueOf(180); + private static final double LOG_2 = Math.log(2); + public CartesianTierPlotter (int tierLevel, IProjector projector, String fieldPrefix) { this.tierLevel = tierLevel; @@ -43,6 +47,11 @@ public class CartesianTierPlotter { setTierBoxes(); setTierVerticalPosDivider(); } + + public CartesianTierPlotter(double radius, IProjector projector, + String fieldPrefix) { + this(CartesianTierPlotter.bestFit(radius), projector, fieldPrefix); + } private void setTierLength (){ this.tierLength = (int) Math.pow(2 , this.tierLevel); @@ -133,17 +142,15 @@ public class CartesianTierPlotter { * Distances less than a mile return 15, finer granularity is * in accurate */ - public int bestFit(double miles){ - - //28,892 a rough circumference of the earth - int circ = 28892; - - double r = miles / 2.0; - - double corner = r - Math.sqrt(Math.pow(r, 2) / 2.0d); - double times = circ / corner; - int bestFit = (int)Math.ceil(log2(times)) + 1; - + static public int bestFit(double range) { + return bestFit(range, DistanceUnits.MILES); + } + + static public int bestFit(double range, DistanceUnits distanceUnit) { + double times = distanceUnit.earthCircumference() / (2.0d * range); + + int bestFit = (int) Math.ceil(log2(times)); + if (bestFit > 15) { // 15 is the granularity of about 1 mile // finer granularity isn't accurate with standard java math @@ -153,12 +160,23 @@ public class CartesianTierPlotter { } /** - * a log to the base 2 formula - * Math.log(value) / Math.log(2) + * Computes log to base 2 of the given value + * * @param value + * Value to compute the log of + * @return Log_2 of the value */ - public double log2(double value) { - - return Math.log(value) / Math.log(2); + public static double log2(double value) { + return Math.log(value) / LOG_2; } + + /** + * Returns the ID of the tier level plotting is occuring at + * + * @return ID of the tier level plotting is occuring at + */ + public int getTierLevelId() { + return this.tierLevel; + } + } diff --git a/lucene/contrib/spatial/src/test/org/apache/lucene/spatial/tier/TestCartesian.java b/lucene/contrib/spatial/src/test/org/apache/lucene/spatial/tier/TestCartesian.java index 7f7e859c473..53288038fb4 100644 --- a/lucene/contrib/spatial/src/test/org/apache/lucene/spatial/tier/TestCartesian.java +++ b/lucene/contrib/spatial/src/test/org/apache/lucene/spatial/tier/TestCartesian.java @@ -141,6 +141,8 @@ public class TestCartesian extends LuceneTestCase { addPoint(writer, "Wonga Wongue Reserve, Gabon", -0.546562,9.459229); addPoint(writer,"Midway Island",25.7, -171.7); addPoint(writer,"North Pole Way",55.0, 4.0); + addPoint(writer,"Close to the North Pole",89.9, 4.0); + addPoint(writer,"Close to the North Pole, other side",89.9, -176.0); writer.commit(); writer.close(); @@ -362,8 +364,8 @@ public class TestCartesian extends LuceneTestCase { System.out.println("Results should be 18 "+ results); } - assertEquals(18, distances.size()); // fixed a store of only needed distances - assertEquals(18, results); + assertEquals(20, distances.size()); // fixed a store of only needed distances + assertEquals(20, results); double lastDistance = 0; for(int i =0 ; i < results; i++){ Document d = searcher.doc(scoreDocs[i].doc); @@ -383,6 +385,103 @@ public class TestCartesian extends LuceneTestCase { lastDistance = geo_distance; } } + + public void testPoles() throws IOException, InvalidGeoException { + searcher = new IndexSearcher(directory, true); + + final double miles = 50.0; + lat = 89.99; + lng = 4; + + if (VERBOSE) System.out.println("testPoleFlipping"); + + // create a distance query + final DistanceQueryBuilder dq = new DistanceQueryBuilder(lat, lng, miles, + latField, lngField, CartesianTierPlotter.DEFALT_FIELD_PREFIX, true, 2, 15); + + if (VERBOSE) System.out.println(dq); + //create a term query to search against all documents + Query tq = new TermQuery(new Term("metafile", "doc")); + + FieldScoreQuery fsQuery = new FieldScoreQuery("geo_distance", Type.FLOAT); + + CustomScoreQuery customScore = new CustomScoreQuery(dq.getQuery(tq),fsQuery){ + + @Override + protected CustomScoreProvider getCustomScoreProvider(IndexReader reader) { + return new CustomScoreProvider(reader) { + @Override // TODO: broken, as reader is not used! + public float customScore(int doc, float subQueryScore, float valSrcScore){ + if (VERBOSE) System.out.println(doc); + if (dq.distanceFilter.getDistance(doc) == null) + return 0; + + double distance = dq.distanceFilter.getDistance(doc); + // boost score shouldn't exceed 1 + if (distance < 1.0d) + distance = 1.0d; + //boost by distance is invertly proportional to + // to distance from center point to location + float score = (float) ((miles - distance) / miles ); + return score * subQueryScore; + } + }; + } + + }; + // Create a distance sort + // As the radius filter has performed the distance calculations + // already, pass in the filter to reuse the results. + // + DistanceFieldComparatorSource dsort = new DistanceFieldComparatorSource(dq.distanceFilter); + Sort sort = new Sort(new SortField("foo", dsort,false)); + + // Perform the search, using the term query, the serial chain filter, and the + // distance sort + TopDocs hits = searcher.search(customScore.createWeight(searcher),null, 1000, sort); + int results = hits.totalHits; + ScoreDoc[] scoreDocs = hits.scoreDocs; + + // Get a list of distances + Map distances = dq.distanceFilter.getDistances(); + + // distances calculated from filter first pass must be less than total + // docs, from the above test of 20 items, 12 will come from the boundary box + // filter, but only 5 are actually in the radius of the results. + + // Note Boundary Box filtering, is not accurate enough for most systems. + + + if (VERBOSE) { + System.out.println("Distance Filter filtered: " + distances.size()); + System.out.println("Results: " + results); + System.out.println("============================="); + System.out.println("Distances should be 18 "+ distances.size()); + System.out.println("Results should be 18 "+ results); + } + + assertEquals(2, distances.size()); // fixed a store of only needed distances + assertEquals(2, results); + double lastDistance = 0; + for(int i =0 ; i < results; i++){ + Document d = searcher.doc(scoreDocs[i].doc); + String name = d.get("name"); + double rsLat = Double.parseDouble(d.get(latField)); + double rsLng = Double.parseDouble(d.get(lngField)); + Double geo_distance = distances.get(scoreDocs[i].doc); + + double distance = DistanceUtils.getInstance().getDistanceMi(lat, lng, rsLat, rsLng); + double llm = DistanceUtils.getInstance().getLLMDistance(lat, lng, rsLat, rsLng); + if (VERBOSE) System.out.println("Name: "+ name +", Distance "+ distance); //(res, ortho, harvesine):"+ distance +" |"+ geo_distance +"|"+ llm +" | score "+ hits.score(i)); + assertTrue(Math.abs((distance - llm)) < 1); + if (VERBOSE) System.out.println("checking limit "+ distance + " < " + miles); + assertTrue((distance < miles )); + if (VERBOSE) System.out.println("checking sort "+ geo_distance + " >= " + lastDistance); + assertTrue(geo_distance >= lastDistance); + lastDistance = geo_distance; + } + } + public void testRange() throws IOException, InvalidGeoException { searcher = new IndexSearcher(directory, true); diff --git a/lucene/contrib/spatial/src/test/org/apache/lucene/spatial/tier/TestCartesianShapeFilter.java b/lucene/contrib/spatial/src/test/org/apache/lucene/spatial/tier/TestCartesianShapeFilter.java index c83289a99eb..38e2ef89f2b 100644 --- a/lucene/contrib/spatial/src/test/org/apache/lucene/spatial/tier/TestCartesianShapeFilter.java +++ b/lucene/contrib/spatial/src/test/org/apache/lucene/spatial/tier/TestCartesianShapeFilter.java @@ -30,7 +30,7 @@ import junit.framework.TestCase; public class TestCartesianShapeFilter extends TestCase { public void testSerializable() throws IOException { - CartesianShapeFilter filter = new CartesianShapeFilter(new Shape("1"), + CartesianShapeFilter filter = new CartesianShapeFilter(new Shape(1), "test"); try { ByteArrayOutputStream bos = new ByteArrayOutputStream();