org.eclipse.jetty.util.resource.CombinedResource Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jetty-util Show documentation
Show all versions of jetty-util Show documentation
Utility classes for Jetty
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.util.resource;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.stream.Collectors;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.URIUtil;
/**
* Multiple {@link Resource} directories presented as a single overlayed {@link Resource} directory.
* This class differs from a {@link List}<{@link Resource}>, as a relative path can {@link #resolve(String) resolve}
* to only a single {@link Resource} in a {@code CombinedResource}, whilst it may resolve to multiple in a
* {@link List}<{@link Resource}>.
*/
public class CombinedResource extends Resource
{
/**
* Make a Resource containing a combination of multiple directory resources.
* @param resources multiple directory resources to combine as a single resource. Order is significant.
* @return A Resource of multiple resources or a single resource if only 1 is passed, or null if none are passed.
* Any Resource returned will always return {@code true} from {@link Resource#isDirectory()}
* @throws IllegalArgumentException if a non-directory resource is passed.
* @see CombinedResource
*/
static Resource combine(List resources)
{
resources = CombinedResource.gatherUniqueFlatResourceList(resources);
if (resources == null || resources.isEmpty())
return null;
if (resources.size() == 1)
return resources.get(0);
return new CombinedResource(resources);
}
static List gatherUniqueFlatResourceList(List resources)
{
if (resources == null || resources.isEmpty())
return null;
List unique = new ArrayList<>(resources.size());
for (Resource r : resources)
{
if (r == null)
{
throw new IllegalArgumentException("Null Resource entry encountered");
}
if (r instanceof CombinedResource resourceCollection)
{
unique.addAll(gatherUniqueFlatResourceList(resourceCollection.getResources()));
}
else
{
if (unique.contains(r))
{
// skip, already seen
continue;
}
if (!r.exists())
{
throw new IllegalArgumentException("Does not exist: " + r);
}
if (!r.isDirectory())
{
throw new IllegalArgumentException("Non-Directory not allowed: " + r);
}
unique.add(r);
}
}
return unique;
}
private final List _resources;
/**
* Instantiates a new resource collection.
*
* @param resources the resources to be added to collection
*/
CombinedResource(List resources)
{
_resources = Collections.unmodifiableList(resources);
}
/**
* Retrieves the resource collection's resources.
*
* @return the resource collection
*/
public List getResources()
{
return _resources;
}
/**
* Resolves a path against the resource collection.
*
* @param subUriPath The path segment to resolve
* @return The resulting resource :
*
* - is a file that exists in at least one of the collection, then the first one found is returned
* - is a directory that exists in at exactly one of the collection, then that simple directory resource is returned
* - is a directory that exists in several of the collection, then a ResourceCollection of those directories is returned
* - is null if not found in any entry in this collection
*
*/
@Override
public Resource resolve(String subUriPath)
{
if (URIUtil.isNotNormalWithinSelf(subUriPath))
throw new IllegalArgumentException(subUriPath);
if (subUriPath.length() == 0 || "/".equals(subUriPath))
{
return this;
}
ArrayList resources = null;
// Attempt a simple (single) Resource lookup that exists
Resource resolved = null;
Resource notFound = null;
for (Resource res : _resources)
{
resolved = res.resolve(subUriPath);
if (!Resources.missing(resolved) && !resolved.isDirectory())
return resolved; // Return simple (non-directory) Resource
if (Resources.missing(resolved) && notFound == null)
notFound = resolved;
if (resources == null)
resources = new ArrayList<>();
resources.add(resolved);
}
if (resources == null)
return notFound; // This will not exist
if (resources.size() == 1)
return resources.get(0);
return new CombinedResource(resources);
}
@Override
public boolean exists()
{
for (Resource r : _resources)
if (r.exists())
return true;
return false;
}
@Override
public Path getPath()
{
int exists = 0;
Path path = null;
for (Resource r : _resources)
{
if (r.exists() && exists++ == 0)
path = r.getPath();
}
return switch (exists)
{
case 0 -> _resources.get(0).getPath();
case 1 -> path;
default -> null;
};
}
@Override
public String getName()
{
return null;
}
@Override
public String getFileName()
{
String filename = null;
// return a non-null filename only if all resources agree on the same name.
for (Resource r : _resources)
{
String fn = r.getFileName();
if (fn == null)
return null;
if (filename == null)
filename = fn;
else if (!filename.equals(fn))
return null;
}
return filename;
}
@Override
public URI getURI()
{
int exists = 0;
URI uri = null;
for (Resource r : _resources)
{
if (r.exists() && exists++ == 0)
uri = r.getURI();
}
return switch (exists)
{
case 0 -> _resources.get(0).getURI();
case 1 -> uri;
default -> null;
};
}
@Override
public boolean isDirectory()
{
return true;
}
@Override
public boolean isReadable()
{
for (Resource r : _resources)
{
if (r.isReadable())
return true;
}
return false;
}
@Override
public Instant lastModified()
{
Instant instant = null;
for (Resource r : _resources)
{
Instant lm = r.lastModified();
if (instant == null || lm.isAfter(instant))
{
instant = lm;
}
}
return instant;
}
@Override
public long length()
{
return -1;
}
@Override
public Iterator iterator()
{
return _resources.iterator();
}
@Override
public List list()
{
Map results = new TreeMap<>();
for (Resource base : _resources)
{
for (Resource r : base.list())
{
if (r.isDirectory())
results.computeIfAbsent(r.getFileName(), this::resolve);
else
results.putIfAbsent(r.getFileName(), r);
}
}
return new ArrayList<>(results.values());
}
@Override
public void copyTo(Path destination) throws IOException
{
Collection all = getAllResources();
for (Resource r : all)
{
if (!r.exists())
continue;
Path relative = getPathTo(r);
Path pathTo = IO.resolvePath(destination, relative);
if (r.isDirectory())
{
IO.ensureDirExists(pathTo);
}
else
{
IO.ensureDirExists(pathTo.getParent());
r.copyTo(pathTo);
}
}
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
CombinedResource other = (CombinedResource)o;
return Objects.equals(_resources, other._resources);
}
@Override
public int hashCode()
{
return Objects.hash(_resources);
}
@Override
public boolean isAlias()
{
for (Resource r : _resources)
{
if (r.isAlias())
return true;
}
return false;
}
@Override
public URI getRealURI()
{
if (!isAlias())
return getURI();
int exists = 0;
URI uri = null;
for (Resource r : _resources)
{
if (r.exists() && exists++ == 0)
uri = r.getRealURI();
}
return switch (exists)
{
case 0 -> _resources.get(0).getRealURI();
case 1 -> uri;
default -> null;
};
}
/**
* @return the list of resources
*/
@Override
public String toString()
{
return _resources.stream()
.map(Resource::toString)
.collect(Collectors.joining(", ", "[", "]"));
}
@Override
public boolean contains(Resource other)
{
// Every resource from the (possibly combined) other resource ...
loop: for (Resource o : other)
{
// Must be contained in at least one of this resources
for (Resource r : _resources)
{
if (r.contains(o))
continue loop;
}
// A resource of the other did not match any in this
return false;
}
return true;
}
@Override
public Path getPathTo(Resource other)
{
Path otherPath = other.getPath();
// If the other resource has a single Path
if (otherPath != null)
{
// return true it's relative location to the first matching resource.
for (Resource r : _resources)
{
if (!r.exists())
continue;
Path path = r.getPath();
if (otherPath.startsWith(path))
return path.relativize(otherPath);
}
return null;
}
// otherwise the other resource must also be some kind of combined resource.
// So every resource in the other combined must have the same relative relationship to us
Path relative = null;
loop : for (Resource o : other)
{
if (!o.exists())
continue;
for (Resource r : _resources)
{
if (!r.exists())
continue;
if (o.getPath().startsWith(r.getPath()))
{
Path rel = r.getPath().relativize(o.getPath());
if (relative == null)
relative = rel;
else if (!relative.equals(rel))
return null;
continue loop;
}
}
return null;
}
return relative;
}
@Override
public boolean isSameFile(Path path)
{
for (Resource r : this)
{
if (r.isSameFile(path))
return true;
}
return false;
}
}