001: /*
002: * Helma License Notice
003: *
004: * The contents of this file are subject to the Helma License
005: * Version 2.0 (the "License"). You may not use this file except in
006: * compliance with the License. A copy of the License is available at
007: * http://adele.helma.org/download/helma/license.txt
008: *
009: * Copyright 1998-2003 Helma Software. All Rights Reserved.
010: *
011: * $RCSfile$
012: * $Author: hannes $
013: * $Revision: 8617 $
014: * $Date: 2007-10-11 12:05:53 +0200 (Don, 11 Okt 2007) $
015: */
016:
017: package helma.framework.core;
018:
019: import helma.objectmodel.db.DbMapping;
020: import helma.util.ResourceProperties;
021: import helma.util.WrappedMap;
022: import helma.framework.repository.Resource;
023: import helma.framework.repository.Repository;
024: import helma.framework.repository.ResourceTracker;
025: import helma.framework.repository.FileResource;
026: import helma.scripting.ScriptingEngine;
027:
028: import java.io.*;
029: import java.util.*;
030:
031: /**
032: * The Prototype class represents Script prototypes/type defined in a Helma
033: * application. This class manages a prototypes templates, functions and actions
034: * as well as optional information about the mapping of this type to a
035: * relational database table.
036: */
037: public final class Prototype {
038: // the app this prototype belongs to
039: Application app;
040:
041: // this prototype's name in natural and lower case
042: String name;
043: String lowerCaseName;
044:
045: // this prototype's resources
046: Resource[] resources;
047:
048: // tells us the checksum of the repositories at the time we last updated them
049: long lastChecksum = -1;
050:
051: // the time at which any of the prototype's files were found updated the last time
052: volatile long lastCodeUpdate = 0;
053:
054: TreeSet code;
055: TreeSet skins;
056:
057: HashMap trackers;
058:
059: TreeSet repositories;
060:
061: // a map of this prototype's skins as raw strings
062: // used for exposing skins to application (script) code (via app.skinfiles).
063: SkinMap skinMap;
064:
065: DbMapping dbmap;
066:
067: private Prototype parent;
068:
069: ResourceProperties props;
070:
071: /**
072: * Creates a new Prototype object.
073: *
074: * @param name the prototype's name
075: * @param repository the first prototype's repository
076: * @param app the application this prototype is a part of
077: */
078: public Prototype(String name, Repository repository, Application app) {
079: // app.logEvent ("Constructing Prototype "+app.getName()+"/"+name);
080: this .app = app;
081: this .name = name;
082: repositories = new TreeSet(app.getResourceComparator());
083: if (repository != null) {
084: repositories.add(repository);
085: }
086: lowerCaseName = name.toLowerCase();
087:
088: // Create and register type properties file
089: props = new ResourceProperties(app);
090: if (repository != null) {
091: props
092: .addResource(repository
093: .getResource("type.properties"));
094: props.addResource(repository.getResource(name
095: + ".properties"));
096: }
097: dbmap = new DbMapping(app, name, props);
098: // we don't need to put the DbMapping into proto.updatables, because
099: // dbmappings are checked separately in TypeManager.checkFiles() for
100: // each request
101:
102: code = new TreeSet(app.getResourceComparator());
103: skins = new TreeSet(app.getResourceComparator());
104:
105: trackers = new HashMap();
106:
107: skinMap = new SkinMap();
108: }
109:
110: /**
111: * Return the application this prototype is a part of
112: */
113: public Application getApplication() {
114: return app;
115: }
116:
117: /**
118: * Adds an repository to the list of repositories
119: * @param repository repository to add
120: * @param update indicates whether to immediately update the prototype with the new code
121: * @throws IOException if reading/updating from the repository fails
122: */
123: public void addRepository(Repository repository, boolean update)
124: throws IOException {
125: if (!repositories.contains(repository)) {
126: repositories.add(repository);
127: props
128: .addResource(repository
129: .getResource("type.properties"));
130: props.addResource(repository.getResource(name
131: + ".properties"));
132: if (update) {
133: RequestEvaluator eval = app
134: .getCurrentRequestEvaluator();
135: ScriptingEngine engine = eval == null ? null
136: : eval.scriptingEngine;
137: Iterator it = repository.getAllResources().iterator();
138: while (it.hasNext()) {
139: checkResource((Resource) it.next(), engine);
140: }
141: }
142: }
143: }
144:
145: /**
146: * Check a prototype for new or updated resources. After this has been
147: * called the code and skins collections of this prototype should be
148: * up-to-date and the lastCodeUpdate be set if there has been any changes.
149: */
150: public synchronized void checkForUpdates() {
151: boolean updatedResources = false;
152:
153: // check if any resource the prototype knows about has changed or gone
154: for (Iterator i = trackers.values().iterator(); i.hasNext();) {
155: ResourceTracker tracker = (ResourceTracker) i.next();
156:
157: try {
158: if (tracker.hasChanged()) {
159: updatedResources = true;
160: // let tracker know we've seen the update
161: tracker.markClean();
162: // if resource has gone remove it
163: if (!tracker.getResource().exists()) {
164: i.remove();
165: String name = tracker.getResource().getName();
166: if (name.endsWith(TypeManager.skinExtension)) {
167: skins.remove(tracker.getResource());
168: } else {
169: code.remove(tracker.getResource());
170: }
171: }
172: }
173: } catch (IOException iox) {
174: iox.printStackTrace();
175: }
176: }
177:
178: // next we check if resources have been created or removed
179: Resource[] resources = getResources();
180:
181: for (int i = 0; i < resources.length; i++) {
182: updatedResources |= checkResource(resources[i], null);
183: }
184:
185: if (updatedResources) {
186: // mark prototype as dirty and the code as updated
187: lastCodeUpdate = System.currentTimeMillis();
188: app.typemgr.setLastCodeUpdate(lastCodeUpdate);
189: }
190: }
191:
192: private boolean checkResource(Resource res, ScriptingEngine engine) {
193: String name = res.getName();
194: boolean updated = false;
195: if (!trackers.containsKey(name)) {
196: if (name.endsWith(TypeManager.templateExtension)
197: || name.endsWith(TypeManager.scriptExtension)
198: || name.endsWith(TypeManager.actionExtension)
199: || name.endsWith(TypeManager.skinExtension)) {
200: updated = true;
201: if (name.endsWith(TypeManager.skinExtension)) {
202: skins.add(res);
203: } else {
204: if (engine != null) {
205: engine.injectCodeResource(lowerCaseName, res);
206: }
207: code.add(res);
208: }
209: trackers.put(res.getName(), new ResourceTracker(res));
210: }
211: }
212: return updated;
213: }
214:
215: /**
216: * Returns the list of resources in this prototype's repositories. Used
217: * by checkForUpdates() to see whether there is anything new.
218: */
219: public Resource[] getResources() {
220: long checksum = getRepositoryChecksum();
221: // reload resources if the repositories checksum has changed
222: if (checksum != lastChecksum) {
223: ArrayList list = new ArrayList();
224: Iterator iterator = repositories.iterator();
225:
226: while (iterator.hasNext()) {
227: try {
228: list.addAll(((Repository) iterator.next())
229: .getAllResources());
230: } catch (IOException iox) {
231: iox.printStackTrace();
232: }
233: }
234:
235: resources = (Resource[]) list.toArray(new Resource[list
236: .size()]);
237: lastChecksum = checksum;
238: }
239: return resources;
240: }
241:
242: /**
243: * Returns an array of repositories containing code for this prototype.
244: */
245: public Repository[] getRepositories() {
246: return (Repository[]) repositories
247: .toArray(new Repository[repositories.size()]);
248: }
249:
250: /**
251: * Get a checksum over this prototype's repositories. This tells us
252: * if any resources were added or removed.
253: */
254: long getRepositoryChecksum() {
255: long checksum = 0;
256: Iterator iterator = repositories.iterator();
257:
258: while (iterator.hasNext()) {
259: try {
260: checksum += ((Repository) iterator.next())
261: .getChecksum();
262: } catch (IOException iox) {
263: iox.printStackTrace();
264: }
265: }
266:
267: return checksum;
268: }
269:
270: /**
271: * Set the parent prototype of this prototype, i.e. the prototype this
272: * prototype inherits from.
273: */
274: public void setParentPrototype(Prototype parent) {
275: // this is not allowed for the hopobject and global prototypes
276: if ("hopobject".equals(lowerCaseName)
277: || "global".equals(lowerCaseName)) {
278: return;
279: }
280:
281: this .parent = parent;
282: }
283:
284: /**
285: * Get the parent prototype from which we inherit, or null
286: * if we are top of the line.
287: */
288: public Prototype getParentPrototype() {
289: return parent;
290: }
291:
292: /**
293: * Check if the given prototype is within this prototype's parent chain.
294: */
295: public final boolean isInstanceOf(String pname) {
296: if (name.equalsIgnoreCase(pname)) {
297: return true;
298: }
299:
300: if (parent != null) {
301: return parent.isInstanceOf(pname);
302: }
303:
304: return false;
305: }
306:
307: /**
308: * Register an object as handler for all our parent prototypes, but only if
309: * a handler by that prototype name isn't registered yet. This is used to
310: * implement direct over indirect prototype precedence and child over parent
311: * precedence.
312: */
313: public final void registerParents(Map handlers, Object obj) {
314:
315: Prototype p = parent;
316:
317: while ((p != null) && !"hopobject".equals(p.getLowerCaseName())) {
318: Object old = handlers.put(p.name, obj);
319: // if an object was already registered by this name, put it back in again.
320: if (old != null) {
321: handlers.put(p.name, old);
322: }
323: // same with lower case name
324: old = handlers.put(p.lowerCaseName, obj);
325: if (old != null) {
326: handlers.put(p.lowerCaseName, old);
327: }
328:
329: p = p.parent;
330: }
331: }
332:
333: /**
334: * Get the DbMapping associated with this prototype
335: */
336: public DbMapping getDbMapping() {
337: return dbmap;
338: }
339:
340: /**
341: * Get a skin for this prototype. This only works for skins
342: * residing in the prototype directory, not for skins files in
343: * other locations or database stored skins. If parentName and
344: * subName are defined, the skin may be a subskin of another skin.
345: */
346: public Skin getSkin(String skinName, String parentName,
347: String subName) throws IOException {
348: Skin skin = null;
349: Resource res = skinMap.getResource(skinName);
350: while (res != null) {
351: skin = Skin.getSkin(res, app);
352: if (skin.hasMainskin())
353: break;
354: res = res.getOverloadedResource();
355: }
356: if (parentName != null) {
357: Skin parentSkin = null;
358: Resource parent = skinMap.getResource(parentName);
359: while (parent != null) {
360: parentSkin = Skin.getSkin(parent, app);
361: if (parentSkin.hasSubskin(subName))
362: break;
363: parent = parent.getOverloadedResource();
364: }
365: if (parent != null) {
366: if (res != null
367: && app.getResourceComparator().compare(res,
368: parent) > 0)
369: return skin;
370: else
371: return parentSkin.getSubskin(subName);
372: }
373: }
374: return skin;
375: }
376:
377: /**
378: * Return this prototype's name
379: *
380: * @return ...
381: */
382: public String getName() {
383: return name;
384: }
385:
386: /**
387: * Return this prototype's name in lower case letters
388: *
389: * @return ...
390: */
391: public String getLowerCaseName() {
392: return lowerCaseName;
393: }
394:
395: /**
396: * Get the last time any script has been re-read for this prototype.
397: */
398: public long lastCodeUpdate() {
399: return lastCodeUpdate;
400: }
401:
402: /**
403: * Signal that some script in this prototype has been
404: * re-read from disk and needs to be re-compiled by
405: * the evaluators.
406: */
407: public void markUpdated() {
408: lastCodeUpdate = System.currentTimeMillis();
409: }
410:
411: /**
412: * Get the prototype's aggregated type.properties
413: *
414: * @return type.properties
415: */
416: public ResourceProperties getTypeProperties() {
417: return props;
418: }
419:
420: /**
421: * Return an iterator over this prototype's code resoruces. Synchronized
422: * to not return a collection in a transient state where it is just being
423: * updated by the type manager.
424: *
425: * @return an iterator of this prototype's code resources
426: */
427: public synchronized Iterator getCodeResources() {
428: // we copy over to a new list, because the underlying set may grow
429: // during compilation through use of app.addRepository()
430: return new ArrayList(code).iterator();
431: }
432:
433: /**
434: * Return an iterator over this prototype's skin resoruces. Synchronized
435: * to not return a collection in a transient state where it is just being
436: * updated by the type manager.
437: *
438: * @return an iterator over this prototype's skin resources
439: */
440: public Iterator getSkinResources() {
441: return skins.iterator();
442: }
443:
444: /**
445: * Return a string representing this prototype.
446: */
447: public String toString() {
448: return "[Prototype " + app.getName() + "/" + name + "]";
449: }
450:
451: /**
452: * Get a map containing this prototype's skins as strings
453: *
454: * @return a scriptable skin map
455: */
456: public Map getScriptableSkinMap() {
457: return new ScriptableSkinMap(new SkinMap());
458: }
459:
460: /**
461: * Get a map containing this prototype's skins as strings, overloaded by the
462: * skins found in the given skinpath.
463: *
464: * @return a scriptable skin map
465: */
466: public Map getScriptableSkinMap(Object[] skinpath) {
467: return new ScriptableSkinMap(new SkinMap(skinpath));
468: }
469:
470: /**
471: * A map of this prototype's skins that acts as a native JavaScript object in
472: * rhino and returns the skins as strings. This is used to expose the skins
473: * to JavaScript in app.skinfiles[prototypeName][skinName].
474: */
475: class ScriptableSkinMap extends WrappedMap {
476:
477: public ScriptableSkinMap(Map wrapped) {
478: super (wrapped);
479: }
480:
481: public Object get(Object key) {
482: Resource res = (Resource) super .get(key);
483:
484: if (res == null || !res.exists()) {
485: return null;
486: }
487:
488: try {
489: return res.getContent();
490: } catch (IOException iox) {
491: return null;
492: }
493: }
494: }
495:
496: /**
497: * A Map that dynamically expands to all skins in this prototype.
498: */
499: class SkinMap extends HashMap {
500: volatile long lastSkinmapLoad = -1;
501: Object[] skinpath;
502:
503: SkinMap() {
504: skinpath = null;
505: }
506:
507: SkinMap(Object[] path) {
508: skinpath = path;
509: }
510:
511: public boolean containsKey(Object key) {
512: checkForUpdates();
513: return super .containsKey(key);
514: }
515:
516: public boolean containsValue(Object value) {
517: checkForUpdates();
518: return super .containsValue(value);
519: }
520:
521: public Set entrySet() {
522: checkForUpdates();
523: return super .entrySet();
524: }
525:
526: public boolean equals(Object obj) {
527: checkForUpdates();
528: return super .equals(obj);
529: }
530:
531: public Skin getSkin(Object key) throws IOException {
532: Resource res = (Resource) get(key);
533:
534: if (res != null) {
535: return Skin.getSkin(res, app);
536: } else {
537: return null;
538: }
539: }
540:
541: public Resource getResource(Object key) {
542: return (Resource) get(key);
543: }
544:
545: public Object get(Object key) {
546: checkForUpdates();
547: return super .get(key);
548: }
549:
550: public int hashCode() {
551: checkForUpdates();
552: return super .hashCode();
553: }
554:
555: public boolean isEmpty() {
556: checkForUpdates();
557: return super .isEmpty();
558: }
559:
560: public Set keySet() {
561: checkForUpdates();
562: return super .keySet();
563: }
564:
565: public Object put(Object key, Object value) {
566: // checkForUpdates ();
567: return super .put(key, value);
568: }
569:
570: public void putAll(Map t) {
571: // checkForUpdates ();
572: super .putAll(t);
573: }
574:
575: public Object remove(Object key) {
576: checkForUpdates();
577: return super .remove(key);
578: }
579:
580: public int size() {
581: checkForUpdates();
582: return super .size();
583: }
584:
585: public Collection values() {
586: checkForUpdates();
587: return super .values();
588: }
589:
590: private void checkForUpdates() {
591: if (lastCodeUpdate > lastSkinmapLoad) {
592: if (lastCodeUpdate == 0) {
593: // if prototype resources haven't been checked yet, check them now
594: Prototype.this .checkForUpdates();
595: }
596: loadSkins();
597: }
598: }
599:
600: private synchronized void loadSkins() {
601: if (lastCodeUpdate == lastSkinmapLoad) {
602: return;
603: }
604:
605: super .clear();
606:
607: // load Skins
608: for (Iterator i = skins.iterator(); i.hasNext();) {
609: Resource res = (Resource) i.next();
610: Resource prev = (Resource) super .put(res.getBaseName(),
611: res);
612: res.setOverloadedResource(prev);
613: }
614:
615: // if skinpath is not null, overload/add skins from there
616: if (skinpath != null) {
617: for (int i = skinpath.length - 1; i >= 0; i--) {
618: if ((skinpath[i] != null)
619: && skinpath[i] instanceof String) {
620: loadSkinFiles((String) skinpath[i]);
621: }
622: }
623: }
624:
625: lastSkinmapLoad = lastCodeUpdate;
626: }
627:
628: private void loadSkinFiles(String skinDir) {
629: File dir = new File(skinDir, Prototype.this .getName());
630: // if directory does not exist use lower case property name
631: if (!dir.isDirectory()) {
632: dir = new File(skinDir, Prototype.this
633: .getLowerCaseName());
634: if (!dir.isDirectory()) {
635: return;
636: }
637: }
638:
639: String[] skinNames = dir.list(app.skinmgr);
640:
641: if ((skinNames == null) || (skinNames.length == 0)) {
642: return;
643: }
644:
645: for (int i = 0; i < skinNames.length; i++) {
646: String name = skinNames[i].substring(0, skinNames[i]
647: .length() - 5);
648: File file = new File(dir, skinNames[i]);
649:
650: Resource res = new FileResource(file);
651: Resource prev = (Resource) super .put(name, res);
652: res.setOverloadedResource(prev);
653: }
654:
655: }
656:
657: public String toString() {
658: return "[SkinMap " + name + "]";
659: }
660: }
661: }
|