/** * Copyright (C) 2013-2014 Project-Vethrfolnir * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.vethrfolnir.services.assets; import java.io.File; import java.lang.reflect.Constructor; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import com.vethrfolnir.logging.MuLogger; import com.vethrfolnir.services.assets.cache.AssetCache; import com.vethrfolnir.services.assets.key.FileKey; import com.vethrfolnir.services.assets.locator.*; import com.vethrfolnir.services.assets.processors.InputStreamProcessor; import corvus.corax.config.CorvusConfig; import corvus.corax.inject.Inject; /** * @author Vlad * * Originally meant for Corvus-Engine */ public final class AssetManager { private static final MuLogger log = MuLogger.getLogger(AssetManager.class); private final ArrayList<AssetLocator> locators = new ArrayList<>(); private final ConcurrentHashMap<Class<? extends AssetCache>, AssetCache> caches = new ConcurrentHashMap<>(); private final ConcurrentHashMap<Class<? extends AssetProcessor>, AssetProcessor> processors = new ConcurrentHashMap<>(); private ExecutorService threadPool; private AtomicInteger threadsInUse = new AtomicInteger(0); private int nrOfThreads = Runtime.getRuntime().availableProcessors(); @Inject private void load() { locators.add(new ClassLocator()); locators.add(new FileLocator()); registerProcessor(new InputStreamProcessor()); // init it; threadPool(); } public <T extends Object> T loadAsset(Class<? extends AssetKey> key, String path) { return loadAsset(key, path, null); } public <T extends Object> T loadAsset(Class<? extends AssetKey> key, String path, Class<T> as) { try { Constructor<? extends AssetKey> cons = key.getConstructor(String.class); AssetKey instance = cons.newInstance(path); return loadAsset(instance, instance.getProcessorType()); } catch (Exception e) { log.fatal("Failed creating dynamic key!", e); } return null; } @SuppressWarnings("unchecked") public <T extends Object> T loadAsset(final AssetKey key, final Class<? extends AssetProcessor> procType) { AssetCache cache = getCache(key.getCacheType()); Object asset; if(cache != null && (asset = cache.getAsset(key)) != null) { return (T) asset; } Future<AssetLoader> task = threadPool().submit(new Callable<AssetLoader>() { @Override public AssetLoader call() throws Exception { // get the result AssetLoader loader = new AssetLoader(key, null); AssetProcessor processor = processors.get(procType); if(processor == null) throw new RuntimeException("No Asset Processor for key["+key.getPath()+"]"); top: // if its not in a cache trigger this for (int i = 0; i < locators.size(); i++) { AssetLocator locator = locators.get(i); Object result = locator.locate(AssetManager.this, key, loader, processor); if(result != null) { loader.result = result; switch (locator.getPriority()) { case Low: continue; case High: break top; } } } return loader; } }); try { AssetLoader loader = task.get(); cache = getCache(key.getCacheType()); if(cache != null) // may be asses with no caches at all. cache.setAsset(key, loader.result); if(loader.result == null) throw new RuntimeException("Asset not found in path = "+key.getPath()+"!"); return (T) loader.result; } catch (InterruptedException | ExecutionException e) { throw new AssetParsingFailException(e); } } /** * <strong>Warning!</strong> This method is meant for resources that are outside jars or any type of archives * @param useCache if true The whole list will be cached. It also affects usage, if useCache cache is set as false it wont try to get anything from cache * @return */ @SuppressWarnings("unchecked") public <T extends Object> ArrayList<T> loadAssets(final AssetKey key, Class<? extends AssetProcessor> procType, boolean useCache) { if(useCache) { AssetCache cache = getCache(key.getCacheType()); if(cache != null) { return new ArrayList<T>((Collection<? extends T>) Arrays.asList(cache.getAssets(key))); } } ArrayList<T> list = new ArrayList<>(); final AssetProcessor processor = processors.get(procType); final File root = new File(CorvusConfig.WorkingDirectory, key.getPath()); final String[] files = root.list(); if(files == null) { // cant get data! log.warn("Cannot find any file in directory["+root+"] Dir exists ? "+root.exists()); return null; } ArrayList<Future<AssetLoader>> tasks = new ArrayList<>(); for (int i = 0; i < files.length; i++) { String fileName = new String(files[i]); File file = new File(key.getPath(), fileName); FileKey fileKey = new FileKey(file.getPath()); final AssetLoader loader = new AssetLoader(fileKey, fileName); Future<AssetLoader> task = threadPool().submit(new Callable<AssetLoader>() { //TODO a wrapper, maybe, or leave it closure for java 8 @Override public AssetLoader call() throws Exception { // get the result top: // if its not in a cache trigger this for (int j = 0; j < locators.size(); j++) { AssetLocator locator = locators.get(j); Object result = locator.locate(AssetManager.this, fileKey, loader, processor); if(result != null) { loader.result = result; switch (locator.getPriority()) { case Low: // Irrelevant. continue; case High: break top; } } } if(loader.result == null) log.fatal("Failed loading an asset file! File["+fileKey.getPath()+"] not found!"); return loader; } }); tasks.add(task); } threadsInUse.addAndGet(tasks.size()); int passes = 0; while(!tasks.isEmpty()) { for(int i = 0; i < tasks.size(); i++) { Future<AssetLoader> task = tasks.get(i); if(task.isDone() && !task.isCancelled()) { try { AssetLoader shell = task.get(); if(shell.result != null) { list.add((T) shell.result); } // else warn? } catch (InterruptedException | ExecutionException e) { log.error("Failed getting asset result! key["+key.getPath()+"]", e); } tasks.remove(i); threadsInUse.decrementAndGet(); } else if(task.isCancelled()) { log.fatal("Thread failed completion! "+key); tasks.remove(i); threadsInUse.decrementAndGet(); } // so it wont lock try { Thread.sleep(60); } catch (InterruptedException e) { log.error("", e); } } if(passes == 1000) { threadsInUse.set(threadsInUse.get() - tasks.size()); tasks.clear(); log.info("Failed loading "+key+". Time out."); break; } passes++; } if(useCache) { AssetCache cache = getCache(key.getCacheType()); if(cache != null) // may be asses with no caches at all. cache.setAssets(key, list.toArray()); } return list; } //TODO: @Finalize public void clean() { locators.clear(); processors.clear(); for(AssetCache cache : caches.values()) cache.clear(); caches.clear(); log.info("Cleanup complete, Preparing threadpool shutdown."); if(threadPool != null && !threadPool.isShutdown()) threadPool.shutdown(); log.info("Threadpool is shutdown."); } public void registerProcessor(AssetProcessor assetProcessor) { processors.put(assetProcessor.getClass(), assetProcessor); } public void removeProcessor(AssetProcessor assetProcessor) { processors.remove(assetProcessor.getClass()); } public void removeProcessor(Class<? extends AssetProcessor> type) { processors.remove(type); } public void registerCache(AssetCache cache) { caches.put(cache.getClass(), cache); } public void removeCache(AssetCache cache) { caches.remove(cache.getClass()); } public void removeCache(Class<? extends AssetCache> type) { caches.remove(type); } /** * @return the threadPool */ private ExecutorService threadPool() { if(threadPool == null || !threadPool.isShutdown()) threadPool = Executors.newFixedThreadPool(nrOfThreads, new ThreadLoader()); return threadPool; } public boolean checkIsThreadPoolNeeded() { if(threadsInUse.get() < 0) { log.fatal("The thread counter is out of range! threadsInUse = "+threadsInUse); threadsInUse.set(0); } if(threadPool != null && threadsInUse.get() <= 0) { try { threadPool.shutdown(); threadPool.awaitTermination(5, TimeUnit.SECONDS); } catch (InterruptedException e) { log.fatal("Failed shutting down threadpool!", e); return false; } threadPool = null; log.info("Shutting down Asset threadpool! No need for it at the moment."); return true; } return false; } /** * @return the threadsInUse */ public int getThreadsInUse() { return threadsInUse.get(); } public AssetCache getCache(Class<? extends AssetCache> type) { if(type == null) return null; return caches.get(type); } /** * @return the locators */ public ArrayList<AssetLocator> getLocators() { return locators; } static class ThreadLoader implements ThreadFactory { private static final AtomicInteger poolNumber = new AtomicInteger(1); private final AtomicInteger threadNumber = new AtomicInteger(1); @Override public Thread newThread(Runnable r) { Thread th = new Thread(r, "AssetLoadingThread-"+(threadNumber.incrementAndGet()+"-Pool-"+poolNumber.incrementAndGet())); //System.out.println(th.getName()); th.setDaemon(true); th.setPriority(Thread.MIN_PRIORITY); return th; } } public class AssetLoader { private final AssetKey key; private final String override; // if any protected Object result; public AssetLoader(AssetKey key, String override) { this.key = key; this.override = override; } /** * @return the key */ public AssetKey getKey() { return key; } /** * @return the override */ public String getOverride() { return override; } } }