Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.rapidoid.io.Res Maven / Gradle / Ivy
/*-
* #%L
* rapidoid-commons
* %%
* Copyright (C) 2014 - 2018 Nikolche Mihajlovski and contributors
* %%
* 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.
* #L%
*/
package org.rapidoid.io;
import org.rapidoid.RapidoidThing;
import org.rapidoid.annotation.Authors;
import org.rapidoid.annotation.Since;
import org.rapidoid.collection.Coll;
import org.rapidoid.env.Env;
import org.rapidoid.log.Log;
import org.rapidoid.u.U;
import org.rapidoid.util.Msc;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
@Authors("Nikolche Mihajlovski")
@Since("4.1.0")
public class Res extends RapidoidThing {
public static volatile Pattern REGEX_INVALID_FILENAME = Pattern.compile("(?:[*?'\"<>|\\x00-\\x1F]|\\.\\.)");
private static final ConcurrentMap FILES = Coll.concurrentMap();
public static final ScheduledThreadPoolExecutor EXECUTOR = new ScheduledThreadPoolExecutor(1);
private static final String[] DEFAULT_LOCATIONS = {""};
private final String name;
private final String[] possibleLocations;
private volatile byte[] bytes;
private volatile long lastUpdatedOn;
private volatile long lastModified;
private volatile String content;
private volatile boolean trackingChanges;
private volatile String cachedFileName;
private volatile Object attachment;
private volatile boolean hidden;
private final Map changeListeners = Coll.synchronizedMap();
private Res(String name, String... possibleLocations) {
this.name = name;
this.possibleLocations = possibleLocations;
validateFilename(name);
}
private static void validateFilename(String filename) {
U.must(!Res.REGEX_INVALID_FILENAME.matcher(filename).find(), "Invalid resource name: %s", filename);
}
public static Res from(File file, String... possibleLocations) {
U.must(!file.isAbsolute() || U.isEmpty(possibleLocations), "Cannot specify locations for an absolute filename!");
return file.isAbsolute() ? absolute(file) : relative(file.getPath(), possibleLocations);
}
public static Res from(String filename, String... possibleLocations) {
return from(new File(filename), possibleLocations);
}
private static Res absolute(File file) {
return create(file.getAbsolutePath());
}
private static Res relative(String filename, String... possibleLocations) {
File file = new File(filename);
if (file.isAbsolute()) {
return absolute(file);
}
if (U.isEmpty(possibleLocations)) {
possibleLocations = DEFAULT_LOCATIONS;
}
U.must(!U.isEmpty(filename), "Resource filename must be specified!");
U.must(!file.isAbsolute(), "Expected relative filename!");
String root = Env.root();
if (U.notEmpty(Env.root())) {
String[] loc = new String[possibleLocations.length * 2];
for (int i = 0; i < possibleLocations.length; i++) {
loc[2 * i] = Msc.path(root, possibleLocations[i]);
loc[2 * i + 1] = possibleLocations[i];
}
possibleLocations = loc;
}
return create(filename, possibleLocations);
}
private static Res create(String filename, String... possibleLocations) {
for (int i = 0; i < possibleLocations.length; i++) {
possibleLocations[i] = Msc.refinePath(possibleLocations[i]);
}
ResKey key = new ResKey(filename, possibleLocations);
Res cachedFile = FILES.get(key);
if (cachedFile == null) {
cachedFile = new Res(filename, possibleLocations);
if (FILES.size() < 1000) {
// FIXME use real cache with proper expiration
FILES.putIfAbsent(key, cachedFile);
}
}
return cachedFile;
}
public synchronized byte[] getBytes() {
loadResource();
mustExist();
return bytes;
}
public byte[] getBytesOrNull() {
loadResource();
return bytes;
}
protected void loadResource() {
// micro-caching the file content, expires after 500ms
if (Msc.timedOut(lastUpdatedOn, 500)) {
boolean hasChanged;
synchronized (this) {
byte[] old = bytes;
byte[] foundRes = null;
if (possibleLocations.length == 0) {
Log.trace("Trying to load the resource", "name", name);
byte[] res = load(name);
if (res != null) {
Log.debug("Loaded the resource", "name", name);
foundRes = res;
this.cachedFileName = name;
}
} else {
for (String location : possibleLocations) {
String filename = Msc.path(location, name);
Log.trace("Trying to load the resource", "name", name, "location", location, "filename", filename);
byte[] res = load(filename);
if (res != null) {
Log.debug("Loaded the resource", "name", name, "file", filename);
foundRes = res;
this.cachedFileName = filename;
break;
}
}
}
if (foundRes == null) {
this.cachedFileName = null;
}
this.bytes = foundRes;
hasChanged = !U.eq(old, bytes) && (old == null || bytes == null || !Arrays.equals(old, bytes));
lastUpdatedOn = U.time();
if (hasChanged) {
content = null;
attachment = null;
}
}
if (hasChanged) {
notifyChangeListeners();
}
}
}
protected byte[] load(String filename) {
File file = IO.file(filename);
if (file.exists()) {
if (!file.isFile() || file.isDirectory()) {
return null;
}
// a normal file on the file system
Log.trace("Resource file exists", "name", name, "file", file);
long lastModif;
try {
lastModif = Files.getLastModifiedTime(file.toPath()).to(TimeUnit.MILLISECONDS);
} catch (IOException e) {
// maybe it doesn't exist anymore
lastModif = U.time();
}
if (lastModif > this.lastModified || !filename.equals(cachedFileName)) {
Log.debug("Loading resource file", "name", name, "file", file);
this.lastModified = file.lastModified();
this.hidden = file.isHidden();
return IO.loadBytes(filename);
} else {
Log.trace("Resource file not modified", "name", name, "file", file);
return bytes;
}
} else {
// it might not exist or it might be on the classpath or compressed in a JAR
Log.trace("Trying to load classpath resource", "name", name, "file", file);
this.hidden = false;
return IO.loadBytes(filename);
}
}
public synchronized String getContent() {
mustExist();
if (content == null) {
byte[] b = getBytes();
content = b != null ? new String(b) : null;
}
return content;
}
public boolean exists() {
loadResource();
return bytes != null;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Res(" + name + ")";
}
public synchronized Reader getReader() {
mustExist();
return new StringReader(getContent());
}
public Res mustExist() {
U.must(exists(), "The file '%s' doesn't exist! Path: %s", name, possibleLocations);
return this;
}
public Res onChange(String name, Runnable listener) {
changeListeners.put(name, listener);
return this;
}
public Res removeChangeListener(String name) {
changeListeners.remove(name);
return this;
}
private void notifyChangeListeners() {
if (!changeListeners.isEmpty()) {
Log.info("Resource has changed, reloading...", "name", name);
}
for (Runnable listener : changeListeners.values()) {
try {
listener.run();
} catch (Throwable e) {
Log.error("Error while processing resource changes!", e);
}
}
}
public synchronized Res trackChanges() {
if (!trackingChanges) {
this.trackingChanges = true;
EXECUTOR.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
// loading the resource causes the resource to check for changes
loadResource();
}
}, 0, 300, TimeUnit.MILLISECONDS);
}
return this;
}
@SuppressWarnings("unchecked")
public T attachment() {
return exists() ? (T) attachment : null;
}
public void attach(Object attachment) {
this.attachment = attachment;
}
public String getCachedFileName() {
return cachedFileName;
}
public static synchronized void reset() {
for (Res res : FILES.values()) {
res.invalidate();
}
FILES.clear();
}
public void invalidate() {
lastUpdatedOn = 0;
}
public boolean isHidden() {
return hidden;
}
}