/*
* Helma License Notice
*
* The contents of this file are subject to the Helma License
* Version 2.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://adele.helma.org/download/helma/license.txt
*
* Copyright 1998-2003 Helma Software. All Rights Reserved.
*
* $RCSfile$
* $Author: hannes $
* $Revision: 9627 $
* $Date: 2009-04-17 22:41:00 +0200 (Fre, 17. Apr 2009) $
*/
package helma.framework.core;
import helma.objectmodel.db.DbMapping;
import helma.util.ResourceProperties;
import helma.util.WrappedMap;
import helma.framework.repository.Resource;
import helma.framework.repository.Repository;
import helma.framework.repository.ResourceTracker;
import helma.framework.repository.FileResource;
import helma.scripting.ScriptingEngine;
import java.io.*;
import java.util.*;
/**
* The Prototype class represents Script prototypes/type defined in a Helma
* application. This class manages a prototypes templates, functions and actions
* as well as optional information about the mapping of this type to a
* relational database table.
*/
public final class Prototype {
// the app this prototype belongs to
Application app;
// this prototype's name in natural and lower case
String name;
String lowerCaseName;
// this prototype's resources
Resource[] resources;
// tells us the checksum of the repositories at the time we last updated them
long lastChecksum = -1;
// the time at which any of the prototype's files were found updated the last time
volatile long lastCodeUpdate = 0;
TreeSet code;
TreeSet skins;
HashMap trackers;
TreeSet repositories;
// a map of this prototype's skins as raw strings
// used for exposing skins to application (script) code (via app.skinfiles).
SkinMap skinMap;
DbMapping dbmap;
private Prototype parent;
ResourceProperties props;
/**
* Creates a new Prototype object.
*
* @param name the prototype's name
* @param repository the first prototype's repository
* @param app the application this prototype is a part of
* @param typeProps custom type mapping properties
*/
public Prototype(String name, Repository repository, Application app, Map typeProps) {
// app.logEvent ("Constructing Prototype "+app.getName()+"/"+name);
this.app = app;
this.name = name;
repositories = new TreeSet(app.getResourceComparator());
if (repository != null) {
repositories.add(repository);
}
lowerCaseName = name.toLowerCase();
// Create and register type properties file
props = new ResourceProperties(app);
if (typeProps != null) {
props.putAll(typeProps);
} else if (repository != null) {
props.addResource(repository.getResource("type.properties"));
props.addResource(repository.getResource(name + ".properties"));
}
dbmap = new DbMapping(app, name, props);
// we don't need to put the DbMapping into proto.updatables, because
// dbmappings are checked separately in TypeManager.checkFiles() for
// each request
code = new TreeSet(app.getResourceComparator());
skins = new TreeSet(app.getResourceComparator());
trackers = new HashMap();
skinMap = new SkinMap();
}
/**
* Return the application this prototype is a part of
*/
public Application getApplication() {
return app;
}
/**
* Adds an repository to the list of repositories
* @param repository repository to add
* @param update indicates whether to immediately update the prototype with the new code
* @throws IOException if reading/updating from the repository fails
*/
public void addRepository(Repository repository, boolean update) throws IOException {
if (!repositories.contains(repository)) {
repositories.add(repository);
props.addResource(repository.getResource("type.properties"));
props.addResource(repository.getResource(name + ".properties"));
if (update) {
RequestEvaluator eval = app.getCurrentRequestEvaluator();
ScriptingEngine engine = eval == null ? null : eval.scriptingEngine;
Iterator it = repository.getAllResources().iterator();
while (it.hasNext()) {
checkResource((Resource) it.next(), engine);
}
}
}
}
/**
* Check a prototype for new or updated resources. After this has been
* called the code and skins collections of this prototype should be
* up-to-date and the lastCodeUpdate be set if there has been any changes.
*/
public synchronized void checkForUpdates() {
boolean updatedResources = false;
// check if any resource the prototype knows about has changed or gone
for (Iterator i = trackers.values().iterator(); i.hasNext();) {
ResourceTracker tracker = (ResourceTracker) i.next();
try {
if (tracker.hasChanged()) {
updatedResources = true;
// let tracker know we've seen the update
tracker.markClean();
// if resource has gone remove it
if (!tracker.getResource().exists()) {
i.remove();
String name = tracker.getResource().getName();
if (name.endsWith(TypeManager.skinExtension)) {
skins.remove(tracker.getResource());
} else {
code.remove(tracker.getResource());
}
}
}
} catch (IOException iox) {
iox.printStackTrace();
}
}
// next we check if resources have been created or removed
Resource[] resources = getResources();
for (int i = 0; i < resources.length; i++) {
updatedResources |= checkResource(resources[i], null);
}
if (updatedResources) {
// mark prototype as dirty and the code as updated
lastCodeUpdate = System.currentTimeMillis();
app.typemgr.setLastCodeUpdate(lastCodeUpdate);
}
}
private boolean checkResource(Resource res, ScriptingEngine engine) {
String name = res.getName();
boolean updated = false;
if (!trackers.containsKey(name)) {
if (name.endsWith(TypeManager.templateExtension) ||
name.endsWith(TypeManager.scriptExtension) ||
name.endsWith(TypeManager.actionExtension) ||
name.endsWith(TypeManager.skinExtension)) {
updated = true;
if (name.endsWith(TypeManager.skinExtension)) {
skins.add(res);
} else {
if (engine != null) {
engine.injectCodeResource(lowerCaseName, res);
}
code.add(res);
}
trackers.put(res.getName(), new ResourceTracker(res));
}
}
return updated;
}
/**
* Returns the list of resources in this prototype's repositories. Used
* by checkForUpdates() to see whether there is anything new.
*/
public Resource[] getResources() {
long checksum = getRepositoryChecksum();
// reload resources if the repositories checksum has changed
if (checksum != lastChecksum) {
ArrayList list = new ArrayList();
Iterator iterator = repositories.iterator();
while (iterator.hasNext()) {
try {
list.addAll(((Repository) iterator.next()).getAllResources());
} catch (IOException iox) {
iox.printStackTrace();
}
}
resources = (Resource[]) list.toArray(new Resource[list.size()]);
lastChecksum = checksum;
}
return resources;
}
/**
* Returns an array of repositories containing code for this prototype.
*/
public Repository[] getRepositories() {
return (Repository[]) repositories.toArray(new Repository[repositories.size()]);
}
/**
* Get a checksum over this prototype's repositories. This tells us
* if any resources were added or removed.
*/
long getRepositoryChecksum() {
long checksum = 0;
Iterator iterator = repositories.iterator();
while (iterator.hasNext()) {
try {
checksum += ((Repository) iterator.next()).getChecksum();
} catch (IOException iox) {
iox.printStackTrace();
}
}
return checksum;
}
/**
* Set the parent prototype of this prototype, i.e. the prototype this
* prototype inherits from.
*/
public void setParentPrototype(Prototype parent) {
// this is not allowed for the hopobject and global prototypes
if ("hopobject".equals(lowerCaseName) || "global".equals(lowerCaseName)) {
return;
}
this.parent = parent;
}
/**
* Get the parent prototype from which we inherit, or null
* if we are top of the line.
*/
public Prototype getParentPrototype() {
return parent;
}
/**
* Check if the given prototype is within this prototype's parent chain.
*/
public final boolean isInstanceOf(String pname) {
if (name.equalsIgnoreCase(pname)) {
return true;
}
if (parent != null) {
return parent.isInstanceOf(pname);
}
return false;
}
/**
* Register an object as handler for all our parent prototypes, but only if
* a handler by that prototype name isn't registered yet. This is used to
* implement direct over indirect prototype precedence and child over parent
* precedence.
*/
public final void registerParents(Map handlers, Object obj) {
Prototype p = parent;
while ((p != null) && !"hopobject".equals(p.getLowerCaseName())) {
Object old = handlers.put(p.name, obj);
// if an object was already registered by this name, put it back in again.
if (old != null) {
handlers.put(p.name, old);
}
// same with lower case name
old = handlers.put(p.lowerCaseName, obj);
if (old != null) {
handlers.put(p.lowerCaseName, old);
}
p = p.parent;
}
}
/**
* Get the DbMapping associated with this prototype
*/
public DbMapping getDbMapping() {
return dbmap;
}
/**
* Get a skin for this prototype. This only works for skins
* residing in the prototype directory, not for skins files in
* other locations or database stored skins. If parentName and
* subName are defined, the skin may be a subskin of another skin.
*/
public Skin getSkin(Prototype proto, String skinname, String subskin, Object[] skinpath)
throws IOException {
Resource res = skinMap.getResource(skinname);
while (res != null) {
Skin skin = Skin.getSkin(res, app);
if (subskin == null && skin.hasMainskin()) {
return skin;
} else if (subskin != null && skin.hasSubskin(subskin)) {
return skin.getSubskin(subskin);
}
String baseskin = skin.getExtends();
if (baseskin != null && !baseskin.equalsIgnoreCase(skinname)) {
// we need to call SkinManager.getSkin() to fetch overwritten
// base skins from skinpath
return app.skinmgr.getSkin(proto, baseskin, subskin, skinpath);
}
res = res.getOverloadedResource();
}
return null;
}
/**
* Return this prototype's name
*
* @return ...
*/
public String getName() {
return name;
}
/**
* Return this prototype's name in lower case letters
*
* @return ...
*/
public String getLowerCaseName() {
return lowerCaseName;
}
/**
* Get the last time any script has been re-read for this prototype.
*/
public long lastCodeUpdate() {
return lastCodeUpdate;
}
/**
* Signal that some script in this prototype has been
* re-read from disk and needs to be re-compiled by
* the evaluators.
*/
public void markUpdated() {
lastCodeUpdate = System.currentTimeMillis();
}
/**
* Set the custom type properties for this prototype and update the database mapping.
* @param map the custom type mapping properties.
*/
public void setTypeProperties(Map map) {
props.clear();
props.putAll(map);
dbmap.update();
}
/**
* Get the prototype's aggregated type.properties
*
* @return type.properties
*/
public ResourceProperties getTypeProperties() {
return props;
}
/**
* Return an iterator over this prototype's code resoruces. Synchronized
* to not return a collection in a transient state where it is just being
* updated by the type manager.
*
* @return an iterator of this prototype's code resources
*/
public synchronized Iterator getCodeResources() {
// we copy over to a new list, because the underlying set may grow
// during compilation through use of app.addRepository()
return new ArrayList(code).iterator();
}
/**
* Return an iterator over this prototype's skin resoruces. Synchronized
* to not return a collection in a transient state where it is just being
* updated by the type manager.
*
* @return an iterator over this prototype's skin resources
*/
public Iterator getSkinResources() {
return skins.iterator();
}
/**
* Return a string representing this prototype.
*/
public String toString() {
return "[Prototype " + app.getName() + "/" + name + "]";
}
/**
* Get a map containing this prototype's skins as strings
*
* @return a scriptable skin map
*/
public Map getScriptableSkinMap() {
return new ScriptableSkinMap(new SkinMap());
}
/**
* Get a map containing this prototype's skins as strings, overloaded by the
* skins found in the given skinpath.
*
* @return a scriptable skin map
*/
public Map getScriptableSkinMap(Object[] skinpath) {
return new ScriptableSkinMap(new SkinMap(skinpath));
}
/**
* A map of this prototype's skins that acts as a native JavaScript object in
* rhino and returns the skins as strings. This is used to expose the skins
* to JavaScript in app.skinfiles[prototypeName][skinName].
*/
class ScriptableSkinMap extends WrappedMap {
public ScriptableSkinMap(Map wrapped) {
super(wrapped);
}
public Object get(Object key) {
Resource res = (Resource) super.get(key);
if (res == null || !res.exists()) {
return null;
}
try {
return res.getContent();
} catch (IOException iox) {
return null;
}
}
}
/**
* A Map that dynamically expands to all skins in this prototype.
*/
class SkinMap extends HashMap {
volatile long lastSkinmapLoad = -1;
Object[] skinpath;
SkinMap() {
skinpath = null;
}
SkinMap(Object[] path) {
skinpath = path;
}
public boolean containsKey(Object key) {
checkForUpdates();
return super.containsKey(key);
}
public boolean containsValue(Object value) {
checkForUpdates();
return super.containsValue(value);
}
public Set entrySet() {
checkForUpdates();
return super.entrySet();
}
public boolean equals(Object obj) {
checkForUpdates();
return super.equals(obj);
}
public Skin getSkin(Object key) throws IOException {
Resource res = (Resource) get(key);
if (res != null) {
return Skin.getSkin(res, app);
} else {
return null;
}
}
public Resource getResource(Object key) {
return (Resource) get(key);
}
public Object get(Object key) {
checkForUpdates();
return super.get(key);
}
public int hashCode() {
checkForUpdates();
return super.hashCode();
}
public boolean isEmpty() {
checkForUpdates();
return super.isEmpty();
}
public Set keySet() {
checkForUpdates();
return super.keySet();
}
public Object put(Object key, Object value) {
// checkForUpdates ();
return super.put(key, value);
}
public void putAll(Map t) {
// checkForUpdates ();
super.putAll(t);
}
public Object remove(Object key) {
checkForUpdates();
return super.remove(key);
}
public int size() {
checkForUpdates();
return super.size();
}
public Collection values() {
checkForUpdates();
return super.values();
}
private void checkForUpdates() {
if (lastCodeUpdate > lastSkinmapLoad) {
if (lastCodeUpdate == 0) {
// if prototype resources haven't been checked yet, check them now
Prototype.this.checkForUpdates();
}
loadSkins();
}
}
private synchronized void loadSkins() {
if (lastCodeUpdate == lastSkinmapLoad) {
return;
}
super.clear();
// load Skins
for (Iterator i = skins.iterator(); i.hasNext();) {
Resource res = (Resource) i.next();
Resource prev = (Resource) super.put(res.getBaseName(), res);
res.setOverloadedResource(prev);
}
// if skinpath is not null, overload/add skins from there
if (skinpath != null) {
for (int i = skinpath.length - 1; i >= 0; i--) {
if ((skinpath[i] != null) && skinpath[i] instanceof String) {
loadSkinFiles((String) skinpath[i]);
}
}
}
lastSkinmapLoad = lastCodeUpdate;
}
private void loadSkinFiles(String skinDir) {
File dir = new File(skinDir, Prototype.this.getName());
// if directory does not exist use lower case property name
if (!dir.isDirectory()) {
dir = new File(skinDir, Prototype.this.getLowerCaseName());
if (!dir.isDirectory()) {
return;
}
}
String[] skinNames = dir.list(app.skinmgr);
if ((skinNames == null) || (skinNames.length == 0)) {
return;
}
for (int i = 0; i < skinNames.length; i++) {
String name = skinNames[i].substring(0, skinNames[i].length() - 5);
File file = new File(dir, skinNames[i]);
Resource res = new FileResource(file);
Resource prev = (Resource) super.put(name, res);
res.setOverloadedResource(prev);
}
}
public String toString() {
return "[SkinMap " + name + "]";
}
}
}