
org.opennms.web.utils.assets.AssetLocatorImpl Maven / Gradle / Ivy
/*
* Licensed to The OpenNMS Group, Inc (TOG) under one or more
* contributor license agreements. See the LICENSE.md file
* distributed with this work for additional information
* regarding copyright ownership.
*
* TOG licenses this file to You under the GNU Affero General
* Public License Version 3 (the "License") or (at your option)
* any later version. You may not use this file except in
* compliance with the License. You may obtain a copy of the
* License at:
*
* https://www.gnu.org/licenses/agpl-3.0.txt
*
* 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 org.opennms.web.utils.assets;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import org.json.JSONArray;
import org.json.JSONObject;
import org.opennms.core.logging.Logging;
import org.opennms.core.utils.StringUtils;
import org.opennms.core.sysprops.SystemProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.context.support.ServletContextResource;
import org.springframework.web.servlet.resource.AbstractResourceResolver;
import org.springframework.web.servlet.resource.ResourceResolverChain;
public class AssetLocatorImpl extends AbstractResourceResolver implements AssetLocator, InitializingBean {
private static Logger LOG = LoggerFactory.getLogger(AssetLocatorImpl.class);
private static AssetLocator s_instance;
private static final Resource s_assetsPath = new ClassPathResource("/assets/");
private ScheduledExecutorService m_executor = null;
private Map> m_unminified = new HashMap<>();
private Map> m_minified = new HashMap<>();
long m_lastModified = 0;
long m_reload;
String m_filesystemPath;
boolean m_useMinified;
public static AssetLocator getInstance() {
return s_instance;
}
public AssetLocatorImpl() {
m_filesystemPath = System.getProperty("org.opennms.web.assets.path");
m_useMinified = Boolean.parseBoolean(System.getProperty("org.opennms.web.assets.minified", "true"));
m_reload = SystemProperties.getLong("org.opennms.web.assets.reload", 5000l);
}
@Override
public long lastModified() {
return m_lastModified;
}
@Override
public Collection getAssets() {
return getAssets(m_useMinified);
}
@Override
public Collection getAssets(final boolean minified) {
return minified? m_minified.keySet() : m_unminified.keySet();
}
@Override
public Optional> getResources(final String assetName) {
return getResources(assetName, m_useMinified);
}
@Override
public Optional> getResources(final String assetName, final boolean minified) {
return Optional.ofNullable(minified? m_minified.get(assetName) : m_unminified.get(assetName));
}
@Override
public Optional getResource(final String assetName, final String type) {
return getResource(assetName, type, m_useMinified);
}
@Override
public Optional getResource(final String assetName, final String type, final boolean minified) {
final Optional> resources = getResources(assetName, minified);
if (resources.isPresent()) {
final Collection r = resources.get();
return r.parallelStream().filter(resource -> {
return type.equals(resource.getType());
}).findFirst();
}
return Optional.empty();
}
@Override
public Optional open(final String assetName, final String type) throws IOException {
return open(assetName, type, m_useMinified);
}
@Override
public Optional open(final String assetName, final String type, final boolean minified) throws IOException {
return withLogPrefix(() -> {
final Optional r = getResource(assetName, type, minified);
if (!r.isPresent()) {
LOG.info("Unable to locate asset resource {}:{}", assetName, type);
return Optional.empty();
}
final AssetResource resource = r.get();
if (m_filesystemPath != null) {
final Path p = Paths.get(m_filesystemPath).resolve(resource.getPath());
LOG.debug("assets path is set, attempting to load {}:{} from {}", assetName, type, p);
if (p.toFile().exists()) {
return Optional.of(new FileInputStream(p.toFile()));
}
}
final String resourcePath = resource.getPath();
LOG.debug("Opening resource {} for asset {}", resourcePath, r);
final URL url = getClass().getResource(resourcePath);
if (url != null) {
return Optional.of(url.openStream());
}
return Optional.of(new ClassPathResource(resourcePath).getInputStream());
});
}
@Override
public void reload() {
final Map> minified = loadAssets(true);
final Map> unminified = loadAssets(false);
if (minified != null) {
m_minified = minified;
}
if (unminified != null) {
m_unminified = unminified;
}
}
@Override
public void afterPropertiesSet() throws Exception {
if (m_reload > 0) {
m_executor = Executors.newSingleThreadScheduledExecutor();
m_executor.scheduleAtFixedRate(() -> {
reload();
}, m_reload, m_reload, TimeUnit.MILLISECONDS);
}
// always load at least once
reload();
// make it easier to reach from JSP pages
s_instance = this;
}
public long getReloadMinutes() {
return m_reload;
}
public void setReloadMinutes(final long minutes) {
m_reload = minutes;
}
public boolean getUseMinified() {
return m_useMinified;
}
public void setUseMinified(final boolean minified) {
m_useMinified= minified;
}
private Map> loadAssets(final boolean minified) {
return withLogPrefix(() -> {
try {
final Map> newAssets = new HashMap<>();
Resource r = new ClassPathResource(minified? "/assets/assets.min.json" : "/assets/assets.json");
if (m_filesystemPath != null) {
final Path p = Paths.get(m_filesystemPath).resolve(minified? "assets.min.json" : "assets.json");
if (p.toFile().exists()) {
r = new FileSystemResource(p.toFile());
}
}
LOG.info("Loading asset data from {}", r);
byte[] bdata = FileCopyUtils.copyToByteArray(r.getInputStream());
final String json = new String(bdata, StandardCharsets.UTF_8);
final JSONObject assetsObj = new JSONObject(json);
final JSONArray names = assetsObj.names();
for (int i=0; i < names.length(); i++) {
final String assetName = names.getString(i);
final JSONObject assetObj = assetsObj.getJSONObject(assetName);
final List assets = new ArrayList<>(assetObj.length());
final JSONArray keys = assetObj.names();
int count = 0;
for (int j=0; j < keys.length(); j++) {
final String type = keys.getString(j);
if (!assetObj.isNull(type)) {
final Object item = assetObj.get(type);
if (item instanceof JSONArray) {
LOG.debug("{} is an anonymous type resource; skipping indexing", type);
} else {
final String path = assetObj.getString(type);
assets.add(new AssetResource(assetName, type, path));
count++;
}
}
}
if (count > 0) {
newAssets.put(assetName, assets);
}
}
m_lastModified = Math.max(getLastModified(), r.lastModified());
return newAssets;
} catch (final Exception e) {
LOG.warn("Failed to load asset manifest.", e);
}
return null;
});
}
private long getLastModified() {
long lastModified = 0;
if (m_filesystemPath != null) {
try (final DirectoryStream stream = Files.newDirectoryStream(Paths.get(m_filesystemPath))) {
for (final Path path : stream) {
lastModified = Math.max(path.toFile().lastModified(), lastModified);
}
} catch (final IOException e) {
LOG.warn("Failed to scan {} for modified files.", m_filesystemPath);
}
}
return lastModified;
}
@Override
protected Resource resolveResourceInternal(final HttpServletRequest request, final String requestPath, final List extends Resource> locations, final ResourceResolverChain chain) {
return getResource(requestPath, locations);
}
protected Resource getResource(final String requestPath, final List extends Resource> locations) {
return withLogPrefix(() -> {
for (final Resource location : locations) {
try {
if (resourcesMatch(s_assetsPath, location)) {
final Resource resource = location.createRelative(requestPath);
LOG.debug("checking request {} in location {}", requestPath, location);
final String fileName = resource.getFilename();
if (m_filesystemPath != null) {
final File f = Paths.get(m_filesystemPath, fileName).toFile();
LOG.debug("Checking for resource in filesystem: {}", f);
if (f.exists() && f.canRead()) {
LOG.trace("File exists and is readable: {}", f);
return new FileSystemResource(f);
}
}
final int index = fileName.lastIndexOf(".");
if (index > 0) {
final String assetName = fileName.substring(0, index);
final String type = fileName.substring(index + 1);
final Optional assetResource = getResource(assetName, type);
LOG.debug("Checking for resource in classpath: {}.{} ({})", assetName, type, assetResource);
if (assetResource.isPresent()) {
final Resource relativeResource = new ClassPathResource("/" + assetResource.get().getPath());
LOG.debug("Using ClassPathResource: {}", relativeResource);
if (relativeResource.exists() && relativeResource.isReadable()) {
LOG.trace("Resource exists and is readable: {}", relativeResource);
return relativeResource;
}
} else {
LOG.debug("Asset resource was not found: {}:{}", assetName, type);
}
}
if (resource.exists()) {
return resource;
}
}
LOG.debug("unhandled location {} for request path {}", location, requestPath);
} catch (final IOException e) {
LOG.debug("Failed to create relative path from {} in {}. Trying next location.", requestPath, location, e);
}
}
return null;
});
}
private boolean resourcesMatch(final Resource a, final Resource b) {
final String aPath = getPath(a);
final String bPath = getPath(b);
if (aPath == null || bPath == null) {
return false;
}
return aPath.equals(bPath);
}
@SuppressWarnings("java:S2259") // sonar doesn't know that StringUtils.hasText null-checks
private String getPath(final Resource resource) {
String ret = null;
if (resource instanceof UrlResource) {
try {
ret = resource.getURL().toExternalForm();
} catch (final IOException e) {
return null;
}
} else if (resource instanceof ClassPathResource) {
ret = ((ClassPathResource) resource).getPath();
} else if (resource instanceof ServletContextResource) {
ret = ((ServletContextResource) resource).getPath();
}
else {
try {
ret = resource.getURL().getPath();
} catch (final IOException e) {
return null;
}
}
if (ret == null) return null;
final var retString = StringUtils.trim(ret);
return retString.startsWith("/")? retString.substring(1) : retString;
}
@Override
protected String resolveUrlPathInternal(final String resourcePath, final List extends Resource> locations, final ResourceResolverChain chain) {
return (StringUtils.hasText(resourcePath) && getResource(resourcePath, locations) != null ? resourcePath : null);
}
private T withLogPrefix(final Callable cb) {
try {
return Logging.withPrefix("web", cb);
} catch (final Exception e) {
if (e instanceof RuntimeException) {
throw (RuntimeException)e;
} else {
throw new RuntimeException(e);
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy