All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.rapidoid.io.Res Maven / Gradle / Ivy

There is a newer version: 5.5.5
Show newest version
/*-
 * #%L
 * rapidoid-commons
 * %%
 * Copyright (C) 2014 - 2017 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;
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy