/*******************************************************************************
* Copyright 2012 Geoscience Australia
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package au.gov.ga.earthsci.worldwind.common.layers.kml.relativeio;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import gov.nasa.worldwind.ogc.kml.io.KMLDoc;
/**
* This class is a wrapper around a {@link String} and {@link KMLDoc} object. It
* allows one to store a relative path, and the {@link KMLDoc} it is relative
* to.
*
* It also contains helper functions for normalizing and relativizing paths.
*
* @author Michael de Hoog (michael.dehoog@ga.gov.au)
*/
public class RelativizedPath
{
public final String path;
public final KMLDoc relativeTo;
public RelativizedPath(String path, KMLDoc relativeTo)
{
this.path = path;
this.relativeTo = relativeTo;
}
/**
* Relativize {@code path} from the {@code document} point in the KML tree.
*
* @param path
* Path to relativize.
* @param document
* Point in the KML tree to relative with respect to.
* @return Relativized path, and the KMLDoc that the relativized path is
* relativized to (usually the {@code document} passed).
*/
public static RelativizedPath relativizePath(String path, RelativeKMLDoc document)
{
/*
* Example:
* - dir/sydney.kmz contains model/model.kml
* - model/model.kml points to ../icon/pin.png
* = should resolve to dir/sydney.kmz/icon/pin.png (dir/sydney.kmz/model/../icon/pin.png)
* = so sydney.kmz doc should return its icon/pin.png
*
* Example 2:
* - dir/sydney.kmz contains model/model.kml
* - model/model.kml points to ../../icon/pin.png
* = should resolve to dir/icon/pin.png (dir/sydney.kmz/model/../../icon/pin.png)
* = so sydney.kmz doc should return the dir/icon/pin.png path
*
* Example 3:
* - dir1/dir2/sydney.kmz contains dir3/dir4/melbourne.kmz
* - dir3/dir4/melbourne.kmz contains model/model.kml
* - model/model.kml points to ../icon/pin.png
* = should resolve to dir1/dir2/sydney.kmz/dir3/dir4/melbourne.kmz/icon/pin.png (dir1/dir2/sydney.kmz/dir3/dir4/melbourne.kmz/model/../icon/pin.png)
* = so melbourne.kmz doc should return its icon/pin.png
*
* Example 4:
* - dir1/dir2/sydney.kmz contains dir3/dir4/melbourne.kmz
* - dir3/dir4/melbourne.kmz contains model/model.kml
* - model/model.kml points to ../../../../icon/pin.png
* = should resolve to dir1/dir2/sydney.kmz/icon/pin.png (dir1/dir2/sydney.kmz/dir3/dir4/melbourne.kmz/model/../../../../icon/pin.png)
* = so sydney.kmz doc should return its icon/pin.png
*
* Example 5:
* - dir1/dir2/sydney.kmz contains dir3/dir4/melbourne.kmz
* - dir3/dir4/melbourne.kmz contains model/model.kml
* - model/model.kml points to ../../../../../../icon/pin.png
* = should resolve to dir1/icon/pin.png (dir1/dir2/sydney.kmz/dir3/dir4/melbourne.kmz/model/../../../../../../icon/pin.png)
* = so sydney.kmz doc should return the dir1/icon/pin.png path
*/
path = normalizePath(path);
String href = document.getHref();
KMLDoc parent = document.getParent();
boolean complete = document.isContainer() && (path.length() < 3 || !path.substring(3).contains(".."));
if (complete || parent == null || !(parent instanceof RelativeKMLDoc) || href == null)
{
//this is as relative as we can go!
//If the document is a container (KMZ), a ../ at the start of a path refers to the
//KMZ as a directory, so remove it and carry on
//Unfortunately, Google Earth supports treating both the KMZ file as the base directory and treating
//the KMZ's parent directory as the base directory (it first checks the first, and if it results in
//a HTTP error, checks the second). This means there are a lot of KMZ files that don't follow spec;
//we should probably add support for this at some stage.
if (document.isContainer() && path.startsWith("../"))
{
path = path.substring(3);
}
return new RelativizedPath(path, document);
}
RelativeKMLDoc relativeParent = (RelativeKMLDoc) parent;
if (document.isContainer())
{
//don't remove the filename from the end of href, as KMZ files (containers) should be treated as a directory
path = normalizePath(href + "/" + path);
return relativizePath(path, relativeParent); //recurse
}
href = normalizePath(href);
int indexOfLastSlash = href.lastIndexOf('/');
String parentHref = indexOfLastSlash >= 0 ? href.substring(0, indexOfLastSlash + 1) : "/";
path = normalizePath(parentHref + path);
return relativizePath(path, relativeParent); //recurse
}
/**
* Normalize a path. This:
* <ul>
* <li>replaces back slashes '\' with forward slashes '/'</li>
* <li>removed repeating slashes '//' (except after a colon ':')</li>
* <li>normalizes the path (dir1/dir2/../dir3 becomes dir1/dir3)</li>
* </ul>
*
* @param path
* @return
*/
public static String normalizePath(String path)
{
if (path == null)
{
return null;
}
//remove any back slashes and double slashes (but not slashes after a ':')
path = path.replaceAll("\\\\+", "/");
path = path.replaceAll("(:/+)|(/)/*", "$1$2");
//remove prefix slash
if (path.startsWith("/"))
{
path = path.substring(1);
}
//fix relative paths that point to a parent directory (with a ".." in the path)
//this acts like unix pathname normalization: dir1/dir2/../dir3 becomes dir1/dir3
if (path.contains(".."))
{
List<String> parts = new ArrayList<String>(Arrays.asList(path.split("/", -1)));
for (int i = 0; i < parts.size(); i++)
{
if (parts.get(i).equals(".."))
{
if (i > 0 && !parts.get(i - 1).equals(".."))
{
parts.remove(i - 1);
parts.remove(i - 1);
i -= 2;
}
}
}
if (!parts.isEmpty())
{
StringBuilder sb = new StringBuilder();
for (String s : parts)
{
sb.append("/" + s);
}
path = sb.substring(1);
}
}
return path;
}
}