001: /*
002: * Copyright 2004-2007 the original author or authors.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: *
016: */
017:
018: package org.jpublish.repository.filesystem;
019:
020: import com.anthonyeden.lib.config.Configuration;
021: import com.anthonyeden.lib.config.ConfigurationException;
022: import com.anthonyeden.lib.config.XMLConfiguration;
023: import com.anthonyeden.lib.util.IOUtilities;
024: import com.atlassian.util.profiling.UtilTimerStack;
025: import org.apache.commons.logging.Log;
026: import org.apache.commons.logging.LogFactory;
027: import org.jpublish.*;
028: import org.jpublish.action.ActionManager;
029: import org.jpublish.action.ActionNotFoundException;
030: import org.jpublish.util.*;
031: import org.jpublish.view.ViewRenderer;
032:
033: import java.io.*;
034: import java.util.Iterator;
035: import java.util.List;
036: import java.util.Locale;
037:
038: /**
039: * The ExtendedFileSystemRepository allows actions to be bound to content
040: * elements through XML configuration files. Only actions bound to dynamic
041: * content elements will be executed.
042: * <p/>
043: * <p>Note: Actions attached to content elements cannot cause an HTTP redirect.
044: * The "redirect" value will be ignored.</p>
045: * <p/>
046: * Florin - refactoring the code for speed and reimplementeing the cache
047: *
048: * @author Anthony Eden
049: * @author <a href="mailto:florin.patrascu@gmail.com">Florin T.PATRASCU</a>
050: */
051:
052: public class ExtendedFileSystemRepository extends
053: AbstractFileSystemRepository implements Configurable {
054: private static final Log log = LogFactory
055: .getLog(ExtendedFileSystemRepository.class);
056: private static final String EMPTY_STRING = "";
057: private static final String DEFAULT_CACHE_NAME = "default";
058: private static final String XML_TYPE = ".xml";
059:
060: private JPublishCache cache = null;
061: private String cacheName = DEFAULT_CACHE_NAME;
062: private String configurationDirectoryName = "config";
063: private static final String DOT = ".";
064: private static final String PROPERTY_NAME = "name";
065: private static final String PROPERTY_VALUE = "value";
066: private static final String PROPERTY_LOCALE = "locale";
067:
068: //private Map configCache = new HashMap();
069:
070: /**
071: * Get the content from the given path. Implementations of this method
072: * should NOT merge the content using view renderer.
073: *
074: * @param path The relative content path
075: * @return The content as a String
076: * @throws Exception Any Exception
077: */
078:
079: public String get(String path) throws Exception {
080: if (log.isDebugEnabled())
081: log.debug("Getting static content element.");
082:
083: //log.info("get: "+path);
084: return loadContent(path);
085: }
086:
087: /**
088: * Get the content from the given path and merge it with the given
089: * context. Any actions attached to the content will be executed
090: * first.
091: *
092: * @param path The content path
093: * @param context The current context
094: * @return The content as a String
095: * @throws Exception Any Exception
096: */
097:
098: public String get(String path, JPublishContext context)
099: throws Exception {
100: UtilTimerStack.push(" ==> /" + path);
101: executeActions(path, context);
102:
103: if (log.isDebugEnabled())
104: log.debug("Getting dynamic content element for path "
105: + path);
106:
107: StringWriter writer = null;
108: BufferedReader reader = null;
109: Reader in = null;
110:
111: try {
112:
113: in = new StringReader(loadContent(path));
114: reader = new BufferedReader(in);
115: writer = new StringWriter();
116: String name = PathUtilities.makeRepositoryURI(getName(),
117: path);
118: ViewRenderer renderer = siteContext.getViewRenderer();
119:
120: renderer.render(context, name, reader, writer);
121:
122: return writer.toString();
123:
124: } catch (FileNotFoundException e) {
125: log.error(e.getMessage());
126: throw new FileNotFoundException("File not found: " + path);
127: } finally {
128: IOUtilities.close(in);
129: IOUtilities.close(reader);
130: IOUtilities.close(writer);
131: UtilTimerStack.pop(" ==> /" + path);
132: }
133: }
134:
135: public void remove(String path) throws Exception {
136: pathToFile(path).delete();
137: }
138:
139: /**
140: * Make the directory for the specified path. Parent directories
141: * will also be created if they do not exist.
142: *
143: * @param path The directory path
144: */
145:
146: public void makeDirectory(String path) {
147: File file = new File(getRealRoot(), path);
148: file.mkdirs();
149: }
150:
151: /**
152: * Remove the directory for the specified path. The directory
153: * must be empty.
154: *
155: * @param path The path
156: * @throws Exception
157: */
158:
159: public void removeDirectory(String path) throws Exception {
160: log.info("Remove directory: " + path);
161: File file = new File(getRealRoot(), path);
162:
163: if (log.isDebugEnabled())
164: log.debug("Deleting file: " + file.getAbsolutePath());
165:
166: if (file.isDirectory()) {
167: file.delete();
168: } else {
169: throw new Exception("Path is not a directory: " + path);
170: }
171: }
172:
173: /**
174: * Get the given content as an InputStream. The InputStream will
175: * return the raw content data.
176: *
177: * @param path The path to the content
178: * @return The InputStream
179: * @throws Exception
180: */
181:
182: public InputStream getInputStream(String path) throws Exception {
183: return new FileInputStream(pathToFile(path));
184: }
185:
186: /**
187: * Get an OutputStream for writing content to the given path.
188: *
189: * @param path The path to the content
190: * @return The OutputStream
191: * @throws Exception
192: */
193:
194: public OutputStream getOutputStream(String path) throws Exception {
195: if (!isWriteAllowed()) {
196: throw new SecurityException("Writing not allowed");
197: }
198:
199: return new FileOutputStream(pathToFile(path));
200: }
201:
202: /**
203: * Get the last modified time in milliseconds for the given path.
204: *
205: * @param path The content path
206: * @return The last modified time in milliseconds
207: * @throws Exception Any Exception
208: */
209:
210: public long getLastModified(String path) throws Exception {
211: return pathToFile(path).lastModified();
212: }
213:
214: /**
215: * Return the configuration directory name.
216: *
217: * @return The configuration directory name
218: */
219:
220: public String getConfigurationDirectoryName() {
221: return configurationDirectoryName;
222: }
223:
224: /**
225: * Set the name of the configuration directory used to locate XML
226: * configuration files for a given path. The default value is "config".
227: * <p/>
228: * <p>Example: using the default value "config", a request for
229: * <code>/site/test.html</code> would look for the configuration file as
230: * <code>/site/config/test.xml</code>.</p>
231: *
232: * @param configurationDirectoryName The new configuration directory name
233: */
234:
235: public void setConfigurationDirectoryName(
236: String configurationDirectoryName) {
237: if (configurationDirectoryName != null) {
238: this .configurationDirectoryName = configurationDirectoryName;
239: }
240: }
241:
242: /**
243: * Load the repository's configuration from the given configuration
244: * object.
245: *
246: * @param configuration The configuration object
247: * @throws Exception
248: */
249:
250: public void loadConfiguration(Configuration configuration)
251: throws Exception {
252: this .name = configuration.getAttribute(PROPERTY_NAME);
253: setRoot(configuration.getChildValue("root"));
254: setCache(configuration.getChildValue("cache"));
255: setWriteAllowed(configuration.getChildValue("write-allowed"));
256: setConfigurationDirectoryName(configuration
257: .getChildValue("config-dir"));
258: }
259:
260: private synchronized void setCache(String cacheName) {
261: this .cacheName = cacheName;
262: JPublishCacheManager jPublishCacheManager = siteContext
263: .getJPublishCacheManager();
264: cache = jPublishCacheManager.getCache(cacheName);
265: }
266:
267: /**
268: * Get an Iterator of paths which are known to the repository.
269: *
270: * @return An iterator of paths
271: * @throws Exception
272: */
273:
274: public Iterator getPaths() throws Exception {
275: return getPaths(EMPTY_STRING);
276: }
277:
278: /**
279: * Get an Iterator of paths which are known to the repository, starting
280: * from the specified base path.
281: *
282: * @param base The base path
283: * @return An iterator of paths
284: * @throws Exception
285: */
286:
287: public Iterator getPaths(String base) throws Exception {
288: return new FileSystemPathIterator(
289: new BreadthFirstFileTreeIterator(pathToFile(base)),
290: this );
291: }
292:
293: /**
294: * Load the content from the given path.
295: *
296: * @param path The path
297: * @return The String
298: */
299:
300: private String loadContent(String path) throws Exception {
301: CacheEntry cacheEntry = (CacheEntry) cache.get(path);
302: long fileTimeStamp = pathToFile(path).lastModified();
303:
304: if (cacheEntry == null
305: || cacheEntry.getLastModified() != fileTimeStamp) {
306: BufferedReader reader = new BufferedReader(
307: new InputStreamReader(getInputStream(path)));// [florin], getInputEncoding(path)));
308: cacheEntry = new CacheEntry(FileCopyUtils
309: .copyToString(reader), fileTimeStamp);
310: cache.put(path, cacheEntry);
311: //log.info(path + " loaded ...");
312: }
313: return (String) cacheEntry.getObject();
314: }
315:
316: /**
317: * Get the File to the content using the path.
318: *
319: * @param path The content path
320: * @return A File object
321: */
322:
323: public File pathToFile(String path) {
324: return new File(getRealRoot(), path);
325:
326: // not sure why I wanted to do this...but for the moment
327: // I am going to use the same system as the FileSystemRepository
328: // for determining the content File object. -AE
329:
330: //return new File(new File(getRealRoot(), "data"), path);
331: }
332:
333: /**
334: * The path to the config is determined by removing the last suffix
335: * from the path and adding <code>.xml</code> to the path and then
336: * appending the path to <code><i>root</i>/conf</code>.
337: *
338: * @param path The path
339: * @return The File
340: */
341:
342: private File pathToConfig(String path) {
343: int dotIndex = path.lastIndexOf(DOT);
344: if (dotIndex > 0) {
345: path = path.substring(0, dotIndex);
346: }
347: return new File(new File(getRealRoot(),
348: getConfigurationDirectoryName()), path + XML_TYPE);
349: }
350:
351: /**
352: * Execute all actions for the given path.
353: * Load the properties file and add the properties to the page definition
354: * [florin]
355: *
356: * @param path The path
357: * @param context The context
358: * @throws Exception Any Exception
359: */
360:
361: private void executeActions(String path, JPublishContext context)
362: throws Exception {
363:
364: ActionManager actionManager = siteContext.getActionManager();
365:
366: String configFileKey;
367: int dotIndex = path.lastIndexOf(DOT);
368: if (dotIndex > 0) {
369: configFileKey = path.substring(0, dotIndex) + XML_TYPE;
370: } else {
371: configFileKey = path + XML_TYPE;
372: }
373:
374: // locate the configuration file
375: File configFile = pathToConfig(path);
376: if (!configFile.exists()) {
377: return;
378: }
379:
380: // load the configuration object, check the cache first
381: Configuration configuration;
382:
383: CacheEntry cacheEntry = (CacheEntry) cache.get(configFileKey);
384: long fileTimeStamp = configFile.lastModified();
385:
386: try {
387: if (cacheEntry == null
388: || cacheEntry.getLastModified() != fileTimeStamp) {
389: configuration = new XMLConfiguration(configFileKey,
390: configFile);
391: configuration.getLocation().setSourceId(configFileKey);
392: cacheEntry = new CacheEntry(configuration,
393: fileTimeStamp);
394: cache.put(configFileKey, cacheEntry);
395: //log.info(configFileKey + " loaded ...");
396: }
397: configuration = (Configuration) cacheEntry.getObject();
398:
399: } catch (ConfigurationException e) {
400: e.printStackTrace();
401: throw new JPublishException(
402: "cannot load the Configuration for: "
403: + configFileKey);
404: } catch (JPublishCacheException e) {
405: e.printStackTrace();
406: throw new JPublishCacheException(
407: "cache refers to a null config object. Required by: "
408: + configFileKey);
409: }
410:
411: Page page = context.getPage();
412:
413: List properties = configuration.getChildren("property");
414: if (properties != null) {
415: Iterator propertyElements = properties.iterator();
416: while (propertyElements.hasNext()) {
417: Configuration propertyElement = (Configuration) propertyElements
418: .next();
419: String name = propertyElement
420: .getAttribute(PROPERTY_NAME);
421: String v = propertyElement.getAttribute(PROPERTY_VALUE);
422: String value = v == null ? propertyElement.getValue()
423: : v;
424: String l = propertyElement
425: .getAttribute(PROPERTY_LOCALE);
426:
427: if (name != null && name.trim().length() > 0) {
428: if (l != null) {
429: try {
430: page
431: .setProperty(name, value,
432: new Locale(l));
433: } catch (Exception e) {
434: throw new Exception(path
435: + ", invalid locale: " + l
436: + ", for element: " + name
437: + ", value: " + value);
438: }
439: } else {
440: page.setProperty(name, value);
441: }
442: } else {
443: throw new Exception(
444: "attempt to define a null property name from: "
445: + configFileKey);
446: }
447: }
448: }
449:
450: // execute all content actions
451: List actions = configuration.getChildren("content-action");
452: if (actions != null) {
453: Iterator contentActionElements = actions.iterator();
454: while (contentActionElements.hasNext()) {
455: Configuration contentActionElement = (Configuration) contentActionElements
456: .next();
457: String actionName = contentActionElement
458: .getAttribute(PROPERTY_NAME);
459: if (actionName != null
460: && actionName.trim().length() > 0) {
461: actionManager.execute(actionName, context,
462: contentActionElement);
463: } else {
464: throw new ActionNotFoundException("Action: "
465: + actionName + ", not found. Defined in: "
466: + configFileKey);
467: }
468: }
469: }
470:
471: }
472:
473: /**
474: * interim solution for using a dual cache; Florin
475: *
476: * @param context
477: */
478: public synchronized void clearCache(JPublishContext context) {
479: try {
480:
481: if (cache != null) {
482: if (context != null) {
483: context.put("old.cache.size", new Integer(cache
484: .getKeys().size()));
485: }
486: cache.clear();
487: }
488: } catch (JPublishCacheException e) {
489: e.printStackTrace();
490: }
491: }
492:
493: public String getCacheName() {
494: return cacheName;
495: }
496: }
|