View Javadoc

1   /**
2    * Copyright (C) 2005-2009 Alfresco Software Limited.
3    *
4    * This file is part of the Spring Surf Extension project.
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *  http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  package org.springframework.extensions.webscripts;
20  
21  import java.io.ByteArrayInputStream;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.InputStreamReader;
25  import java.io.Reader;
26  import java.io.StringReader;
27  import java.util.ArrayList;
28  import java.util.HashMap;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.StringTokenizer;
32  
33  import org.apache.commons.logging.Log;
34  import org.apache.commons.logging.LogFactory;
35  import org.springframework.extensions.surf.exception.ConnectorProviderException;
36  import org.springframework.extensions.surf.exception.RemoteConfigException;
37  import org.springframework.extensions.surf.exception.WebScriptsPlatformException;
38  import org.springframework.extensions.surf.util.URLEncoder;
39  import org.springframework.extensions.webscripts.connector.Connector;
40  import org.springframework.extensions.webscripts.connector.ConnectorContext;
41  import org.springframework.extensions.webscripts.connector.ConnectorProvider;
42  import org.springframework.extensions.webscripts.connector.ConnectorProviderImpl;
43  import org.springframework.extensions.webscripts.connector.ConnectorService;
44  import org.springframework.extensions.webscripts.connector.HttpMethod;
45  import org.springframework.extensions.webscripts.connector.Response;
46  
47  import freemarker.cache.TemplateLoader;
48  
49  /**
50   * Store implementation that queries and retrieves documents from a remote HTTP endpoint.
51   * <p>
52   * The endpoint is assumed to support a WebScript Remote Store implementation (such as
53   * AVMRemoteStore) that mirrors the required Store API. 
54   * 
55   * @author Kevin Roast
56   */
57  public class RemoteStore extends AbstractStore
58  {
59  	private static Log logger = LogFactory.getLog(RemoteStore.class);
60      
61      public static final String DEFAULT_API = "/remotestore";
62      public static final String DEFAULT_ENDPOINT_ID = "alfresco";
63  	
64      private static final String API_LISTPATTERN = "listpattern";
65      private static final String API_LISTALL = "listall";
66      private static final String API_GET = "get";
67      private static final String API_CREATE = "create";
68      private static final String API_DELETE = "delete";
69      private static final String API_UPDATE = "update";
70      private static final String API_LASTMODIFIED = "lastmodified";
71      private static final String API_HAS = "has";
72      
73      private ConnectorService connectorService;
74      private ConnectorProvider connectorProvider;
75      
76      private String storeId;
77      private String endpoint;    
78      private String path;
79      private String api;
80      private String webappId;
81      private String webappPathPrefix;
82          
83      /**
84       * @param service   The ConnectorService bean
85       */
86      public void setConnectorService(ConnectorService service)
87      {
88          this.connectorService = service;
89      }
90      
91      /**
92       * Gets the connector service.
93       * 
94       * @return the connector service
95       */
96      public ConnectorService getConnectorService()
97      {
98      	return this.connectorService;
99      }
100         
101     /**
102      * Sets the connector provider
103      */
104     public void setConnectorProvider(ConnectorProvider connectorProvider)
105     {
106     	this.connectorProvider = connectorProvider;
107     }
108     
109     /**
110      * @return the connector provider
111      */
112     public ConnectorProvider getConnectorProvider()
113     {
114     	return this.connectorProvider;
115     }
116     
117     /**
118      * @param api the WebScript API path to set for the remote store i.e. "/remotestore"
119      */
120     public void setApi(String api)
121     {
122     	this.api = api;
123     }
124     
125     /**
126      * Gets the api.
127      * 
128      * @return the api
129      */
130     public String getApi()
131     {
132     	return this.api;
133     }
134         
135     /**
136      * Sets the base path to send to the remote store
137      * 
138      * @param path 
139      */
140     public void setPath(String path)
141     {
142     	this.path = path;
143     }
144             
145     /**
146      * @param endpoint the endpoint ID to use when calling the remote API
147      */
148     public void setEndpoint(String endpoint)
149     {
150     	this.endpoint = endpoint;
151     }
152     
153     /**
154      * Gets the endpoint.
155      * 
156      * @return the endpoint
157      */
158     public String getEndpoint()
159     {
160     	return this.endpoint;
161     }
162     
163     /**
164      * Sets the store's web application id to bind to within the designated store
165      * This is meaningful for WCM Web Project stores.
166      * 
167      * @param webappId
168      */
169     public void setWebappId(String webappId)
170     {
171     	this.webappId = webappId;
172     }
173     
174     public String getWebappPathPrefix()
175     {
176         return this.webappPathPrefix;
177     }
178     
179     public void setWebappPathPrefix(String webappPathPrefix)
180     {
181         this.webappPathPrefix = webappPathPrefix;
182     }
183     
184     /**
185      * Gets the store's web application id binding
186      * This is meaningful for WCM Web Project stores.
187      * 
188      * @return
189      */
190     public String getWebappId()
191     {
192     	String value = this.webappId;
193     	
194     	if (value == null && this.getPreviewContext() != null)
195     	{
196     		value = getPreviewContext().getWebappId();
197     	}
198     	
199     	return value;
200     }
201         
202     /**
203      * Allows for specification of default or fallback store id to use
204      * when binding to a remote store.  This can be overridden by providing
205      * an implementation of a RemoteStoreContextProvider in the Spring
206      * Bean configuration.
207      * 
208      * @param storeId   the default store id
209      */
210     public void setStoreId(String storeId)
211     {
212     	this.storeId = storeId;
213     }
214     
215     /**
216      * Gets the store id.
217      * 
218      * @return the store id
219      */
220     public String getStoreId()
221     {
222     	String value = this.storeId;
223     	
224     	if (value == null && getPreviewContext() != null)
225     	{
226     		value = getPreviewContext().getStoreId();
227     	}
228     	
229     	return value;
230     }
231     
232     /**
233      * Store path calculation
234      * 
235      * If we have a store context, then we can check to see if a base path should be inserted ahead of the
236      * path that we believe we're directing to.
237      * 
238      * Use case - consider writing a file /alfresco/site-data/components/component.xml
239      * 
240      * If we're writing to sitestore, then the file is stored relative to the store root.
241      * 
242      * In the case of a WCM web project, however, we may want to persist to one of several web applications.
243      * If we have a webappId (retrieved from the context), then we prepend: /WEB-INF/classes
244      * 
245      * The new location is: /WEB-INF/classes/alfresco/site-data/components/component.xml
246      * 
247      * This allows us to operate against both straight up AVM stores as well as WCM Web Project AVM stores.
248      * 
249      * @return
250      */
251     public String getStorePath()
252     {
253     	String value = this.path;
254     	
255     	// if we have a webapp id to which we are binding, we will use
256     	// the WCM webapp path prefix to "move" our starting point
257     	// to the appropriate place in the avm store
258     	if (this.getStoreId() != null && this.getWebappId() != null)
259     	{
260 			value = this.getWebappPathPrefix();
261 			if (value != null)
262 			{
263     			if (!value.endsWith("/"))
264     			{
265     				value += "/";
266     			}
267     			if (this.path.startsWith("/"))
268     			{
269     				value += this.path.substring(1);
270     			}
271     			else
272     			{
273     				value += this.path;
274     			}
275 			}
276     	}
277     	
278     	if (value == null)
279     	{
280     	    value = "";
281     	}
282     	
283     	return value;
284     }
285         
286     /* (non-Javadoc)
287      * @see org.alfresco.web.scripts.Store#init()
288      */
289     public void init()
290     {
291         if (this.connectorService == null)
292         {
293             throw new IllegalArgumentException("ConnectorService reference is mandatory for RemoteStore.");
294         }
295         if (this.getEndpoint() == null || getEndpoint().length() == 0)
296         {
297             throw new IllegalArgumentException("Endpoint ID is mandatory for RemoteStore.");
298         }
299         if (this.getApi() == null || this.getApi().length() == 0)
300         {
301             throw new IllegalArgumentException("API name is mandatory for RemoteStore.");
302         }
303         if (this.getStorePath() == null)
304         {
305             throw new IllegalArgumentException("Path prefix is mandatory for RemoteStore.");
306         }
307     	
308         if (logger.isDebugEnabled())
309         {
310             logger.debug("RemoteStore initialised with endpoint id '" + this.getEndpoint() + "' API path '" +
311                          this.getApi() + "' path prefix '" + this.getStorePath() + "'.");
312         }
313     }
314 
315     /* (non-Javadoc)
316      * @see org.alfresco.web.scripts.Store#isSecure()
317      */
318     public boolean isSecure()
319     {
320         return false;
321     }
322     
323     /* (non-Javadoc)
324      * @see org.alfresco.web.scripts.Store#exists()
325      */
326     public boolean exists()
327     {
328         // always return true - even if a remote store appears to be down we cannot
329         // assume this is always the case and must retry until it is restored
330         return true;
331     }
332 
333     /* (non-Javadoc)
334      * @see org.alfresco.web.scripts.Store#hasDocument(java.lang.String)
335      */
336     public boolean hasDocument(String documentPath) throws IOException
337     {
338         boolean hasDocument = false;
339         
340         Response res = callGet(buildEncodeCall(API_HAS, documentPath));
341         if (Status.STATUS_OK == res.getStatus().getCode())
342         {
343             hasDocument = Boolean.parseBoolean(res.getResponse());
344         }
345         else
346         {
347             throw new IOException("Unable to test document path: " + documentPath +
348                     " in remote store: " + this.getEndpoint() +
349                     " due to error: " + res.getStatus().getCode() + " " + res.getStatus().getMessage());
350         }
351         
352         if (logger.isDebugEnabled())
353             logger.debug("RemoteStore.hasDocument() " + documentPath + " = " + hasDocument);
354         
355         return hasDocument;
356     }
357 
358     /* (non-Javadoc)
359      * @see org.alfresco.web.scripts.Store#lastModified(java.lang.String)
360      */
361     public long lastModified(String documentPath) throws IOException
362     {
363         Response res = callGet(buildEncodeCall(API_LASTMODIFIED, documentPath));
364         if (Status.STATUS_OK == res.getStatus().getCode())
365         {
366             try
367             {
368                 long lastMod = Long.parseLong(res.getResponse());
369                 
370                 if (logger.isDebugEnabled())
371                     logger.debug("RemoteStore.lastModified() " + documentPath + " = " + lastMod);
372                 
373                 return lastMod;
374             }
375             catch (NumberFormatException ne)
376             {
377                 throw new IOException("Failed to process lastModified response: " + ne.getMessage());
378             }
379         }
380         else
381         {
382             throw new IOException("Unable to get lastModified date of document path: " + documentPath +
383                     " in remote store: " + this.getEndpoint() +
384                     " due to error: " + res.getStatus().getCode() + " " + res.getStatus().getMessage());
385         }
386     }
387 
388     /* (non-Javadoc)
389      * @see org.alfresco.web.scripts.Store#updateDocument(java.lang.String, java.lang.String)
390      */
391     public void updateDocument(String documentPath, String content) throws IOException
392     {
393         ByteArrayInputStream in = new ByteArrayInputStream(content.getBytes("UTF-8"));
394         Response res = callPost(buildEncodeCall(API_UPDATE, documentPath), in);
395         
396         if (logger.isDebugEnabled())
397             logger.debug("RemoteStore.updateDocument() " + documentPath + " = " + res.getStatus().getCode());
398         
399         if (Status.STATUS_OK != res.getStatus().getCode())
400         {
401             throw new IOException("Unable to update document path: " + documentPath +
402                     " in remote store: " + this.getEndpoint() +
403                     " due to error: " + res.getStatus().getCode() + " " + res.getStatus().getMessage());
404         }
405     }
406 
407     /* (non-Javadoc)
408      * @see org.alfresco.web.scripts.Store#removeDocument(java.lang.String)
409      */
410     public boolean removeDocument(String documentPath) throws IOException
411     {
412         Response res = callDelete(buildEncodeCall(API_DELETE, documentPath));
413         
414         boolean removed = (Status.STATUS_OK == res.getStatus().getCode());
415         
416         if (logger.isDebugEnabled())
417             logger.debug("RemoteStore.removeDocument() " + documentPath + " = " + res.getStatus().getCode() + " (removed = "+removed+")");
418         
419         return removed;
420     }
421 
422     /* (non-Javadoc)
423      * @see org.alfresco.web.scripts.Store#createDocument(java.lang.String, java.lang.String)
424      */
425     public void createDocument(String documentPath, String content) throws IOException
426     {
427         ByteArrayInputStream in = new ByteArrayInputStream(content.getBytes("UTF-8"));
428         Response res = callPost(buildEncodeCall(API_CREATE, documentPath), in);
429         
430         if (logger.isDebugEnabled())
431             logger.debug("RemoteStore.createDocument() " + documentPath + " = " + res.getStatus().getCode());
432         
433         if (Status.STATUS_OK != res.getStatus().getCode())
434         {
435             throw new IOException("Unable to create document path: " + documentPath +
436                     " in remote store: " + this.getEndpoint() +
437                     " due to error: " + res.getStatus().getCode() + " " + res.getStatus().getMessage());
438         }
439     }
440 
441     /* (non-Javadoc)
442      * @see org.alfresco.web.scripts.Store#getDocument(java.lang.String)
443      */
444     public InputStream getDocument(String documentPath) throws IOException
445     {
446         return getDocumentResponse(documentPath).getResponseStream();
447     }
448     
449     private Response getDocumentResponse(String path)
450         throws IOException
451     {
452         Response res = callGet(buildEncodeCall(API_GET, path));
453         
454         if (logger.isDebugEnabled())
455             logger.debug("RemoteStore.getDocument() " + path + " = " + res.getStatus().getCode());
456         
457         if (Status.STATUS_OK == res.getStatus().getCode())
458         {
459             return res;
460         }
461         else
462         {
463             throw new IOException("Unable to retrieve document path: " + path +
464                     " in remote store: " + this.getEndpoint() +
465                     " due to error: " + res.getStatus().getCode() + " " + res.getStatus().getMessage());
466         }
467     }
468 
469     /* (non-Javadoc)
470      * @see org.alfresco.web.scripts.Store#getAllDocumentPaths()
471      */
472     public String[] getAllDocumentPaths()
473     {
474         Response res = callGet(buildEncodeCall(API_LISTALL, ""));
475         
476         if (logger.isDebugEnabled())
477             logger.debug("RemoteStore.getAllDocumentPaths() " + res.getStatus().getCode());
478         
479         if (Status.STATUS_OK == res.getStatus().getCode())
480         {
481             // convert to an array of store root-relative paths
482             List<String> list = new ArrayList<String>(128);
483             StringTokenizer t = new StringTokenizer(res.getResponse(), "\n");
484             while (t.hasMoreTokens())
485             {
486                 list.add(t.nextToken());
487             }            
488             String[] paths = list.toArray(new String[list.size()]);
489             
490             // truncate paths so that they are relative to the store path
491             // and invariant of the type of AVM store (WCM or plain)
492             convertToRelativePaths(paths);
493             
494             return paths;
495         }
496         else
497         {
498             return new String[0];
499         }
500     }
501 
502     /* (non-Javadoc)
503      * @see org.alfresco.web.scripts.Store#getDocumentPaths(java.lang.String, boolean, java.lang.String)
504      */
505     public String[] getDocumentPaths(String path, boolean includeSubPaths, String documentPattern)
506     {
507         Map<String, String> args = new HashMap<String, String>(1, 1.0f);
508         args.put("m", documentPattern);
509         Response res = callGet(buildEncodeCall(API_LISTPATTERN, path, args));
510         
511         if (logger.isDebugEnabled())
512             logger.debug("RemoteStore.getDocumentPaths() " + path + " subpaths: " + includeSubPaths +
513                          " pattern: " + documentPattern + " = " + res.getStatus().getCode() + " " + res.getStatus().getMessage());
514         
515         if (Status.STATUS_OK == res.getStatus().getCode())
516         {
517             // convert to an array of store root-relative paths
518             List<String> list = new ArrayList<String>(128);
519             StringTokenizer t = new StringTokenizer(res.getResponse(), "\n");
520             while (t.hasMoreTokens())
521             {
522                 list.add(t.nextToken());
523             }            
524             String[] paths = list.toArray(new String[list.size()]);
525             
526             // truncate paths so that they are relative to the store path
527             // and invariant of the type of AVM store (WCM or plain)
528             convertToRelativePaths(paths);
529             
530             return paths;
531         }
532         else
533         {
534             return new String[0];
535         }
536     }
537 
538     /* (non-Javadoc)
539      * @see org.alfresco.web.scripts.Store#getDescriptionDocumentPaths()
540      */
541     public String[] getDescriptionDocumentPaths()
542     {
543         return getDocumentPaths("", true, "*.desc.xml");
544     }
545 
546     /* (non-Javadoc)
547      * @see org.alfresco.web.scripts.Store#getScriptDocumentPaths(org.alfresco.web.scripts.WebScript)
548      */
549     public String[] getScriptDocumentPaths(WebScript script)
550     {
551         String scriptPaths = script.getDescription().getId() + ".*";
552         return getDocumentPaths("", false, scriptPaths);
553     }
554 
555     /* (non-Javadoc)
556      * @see org.alfresco.web.scripts.Store#getScriptLoader()
557      */
558     public ScriptLoader getScriptLoader()
559     {
560         return new RemoteStoreScriptLoader();
561     }
562 
563     /* (non-Javadoc)
564      * @see org.alfresco.web.scripts.Store#getTemplateLoader()
565      */
566     public TemplateLoader getTemplateLoader()
567     {
568         return new RemoteStoreTemplateLoader();
569     }
570 
571     /* (non-Javadoc)
572      * @see org.alfresco.web.scripts.Store#getBasePath()
573      */
574     public String getBasePath()
575     {
576     	return getStorePath();
577     }
578 
579 
580     /**
581      * Helper to build and encode a remote store call
582      * 
583      * @param method        Remote store method name
584      * @param documentPath  Document path to encode
585      * 
586      * @return encoded URL to execute
587      */
588     private String buildEncodeCall(String method, String documentPath)
589     {
590         return buildEncodeCall(method, documentPath, null);
591     }
592     
593     /**
594      * Helper to build and encode a remote store call
595      * 
596      * @param method        Remote store method name
597      * @param documentPath  Document path to encode, can be empty but not null
598      * @param args          Args map to apply to URL call, can be null or empty
599      * 
600      * @return encoded URL to execute
601      */
602     private String buildEncodeCall(String method, String documentPath, Map<String, String> args)
603     {
604         StringBuilder buf = new StringBuilder(128);
605         
606         buf.append(this.getApi());
607         buf.append('/');
608         buf.append(method);
609         
610         // encode store path into url
611         String fullPath = this.getStorePath() + "/" + documentPath;        
612         for (StringTokenizer t = new StringTokenizer(fullPath, "/"); t.hasMoreTokens(); /**/)
613         {
614             buf.append('/').append(URLEncoder.encode(t.nextToken()));
615         }
616         
617         // Append in the store id
618         String storeId = this.getStoreId();
619         if (storeId != null)
620         {
621     		if (args == null)
622     		{
623     			args = new HashMap<String, String>(1, 1.0f);
624     		}
625    			args.put("s", storeId);
626         }
627         
628         // Append in the webapp id (if applicable)
629         String webappId = this.getWebappId();
630         if (webappId != null)
631         {
632     		if (args == null)
633     		{
634     			args = new HashMap<String, String>(1, 1.0f);
635     		}
636    			args.put("w", webappId);        	
637         }
638                 
639         // append in any request parameters
640         if (args != null && args.size() != 0)
641         {
642             buf.append('?');
643             int count = 0;
644             for (Map.Entry<String, String> entry : args.entrySet())
645             {
646                 if (count++ != 0)
647                 {
648                     buf.append('&');
649                 }
650                 buf.append(entry.getKey()).append('=').append(URLEncoder.encode(entry.getValue()));
651             }
652         }
653         
654         return buf.toString();
655     }
656 
657     /**
658      * Perform a POST call to the given URI with the supplied input.
659      */
660     private Response callPost(String uri, InputStream in)
661     {
662         try
663         {
664             Connector con = getConnector();
665             return con.call(uri, null, in);
666         }
667         catch (ConnectorProviderException cpe)
668         {
669             throw new WebScriptsPlatformException("Unable to find config for remote store.", cpe);
670         }
671     }
672 
673     /**
674      * Perform a GET call to the given URI.
675      */
676     private Response callGet(String uri)
677     {
678         try
679         {
680             Connector con = getConnector();
681             return con.call(uri);
682         }
683         catch (ConnectorProviderException cpe)
684         {
685             throw new WebScriptsPlatformException("Unable to find config for remote store.", cpe);
686         }
687     }
688     
689     /**
690      * Perform a DELETE call to the given URI.
691      */
692     private Response callDelete(String uri)
693     {
694         try
695         {
696             Connector con = getConnector();
697             ConnectorContext context = new ConnectorContext(HttpMethod.DELETE, null, null);
698             return con.call(uri, context);
699         }
700         catch (ConnectorProviderException cpe)
701         {
702             throw new WebScriptsPlatformException("Unable to find config for remote store.", cpe);
703         }
704     }
705 
706     /**
707      * Get a Connector for access to the endpoint. If a connector has been bound to the
708      * current thread then use it, else retrieve a transient connector instance from the
709      * ConnectorService.
710      * 
711      * @return Connector
712      * 
713      * @throws RemoteConfigException
714      */
715     private Connector getConnector() throws ConnectorProviderException
716     {
717     	Connector conn = null;
718     	
719     	// use a default connector provider if none injected
720     	if (connectorProvider == null)
721     	{
722     	    connectorProvider = new ConnectorProviderImpl();
723     	}
724 
725     	// provision connector
726    		conn = getConnectorProvider().provide(this.getEndpoint());
727         
728         return conn; 
729     }
730     
731     
732     /**
733      * Remote Store implementation of a Script Loader
734      * 
735      * @author Kevin Roast
736      */
737     protected class RemoteStoreScriptLoader implements ScriptLoader
738     {
739         /**
740          * @see org.springframework.extensions.webscripts.ScriptLoader#getScript(java.lang.String)
741          */
742         public ScriptContent getScript(String path)
743         {
744             ScriptContent sc = null;
745             try
746             {
747                 if (hasDocument(path))
748                 {
749                     sc = new RemoteScriptContent(path);
750                 }
751             }
752             catch (IOException e)
753             {
754                 throw new WebScriptException("Error locating script " + path, e);                
755             }
756             return sc;
757         }
758     }
759     
760     
761     /**
762      * Remote Store implementation of a Template Loader
763      * 
764      * @author Kevin Roast
765      */
766     private class RemoteStoreTemplateLoader implements TemplateLoader
767     {
768         /**
769          * @see freemarker.cache.TemplateLoader#closeTemplateSource(java.lang.Object)
770          */
771         public void closeTemplateSource(Object templateSource) throws IOException
772         {
773             // nothing to do - we return a reader to fully retrieved in-memory data
774         }
775 
776         /**
777          * @see freemarker.cache.TemplateLoader#findTemplateSource(java.lang.String)
778          */
779         public Object findTemplateSource(String name) throws IOException
780         {
781             RemoteStoreTemplateSource source = null;
782             if (hasDocument(name))
783             {
784                 source = new RemoteStoreTemplateSource(name);
785             }
786             return source;
787         }
788 
789         /**
790          * @see freemarker.cache.TemplateLoader#getLastModified(java.lang.Object)
791          */
792         public long getLastModified(Object templateSource)
793         {
794             return ((RemoteStoreTemplateSource)templateSource).lastModified();
795         }
796 
797         /**
798          * @see freemarker.cache.TemplateLoader#getReader(java.lang.Object, java.lang.String)
799          */
800         public Reader getReader(Object templateSource, String encoding) throws IOException
801         {
802             return ((RemoteStoreTemplateSource)templateSource).getReader(encoding);
803         }
804     }
805     
806     
807     /**
808      * Template Source - loads from a Remote Store.
809      * 
810      * TODO: implement caching of remotely loaded template content?
811      * 
812      * @author Kevin Roast
813      */
814     private class RemoteStoreTemplateSource
815     {
816         private String templatePath;
817         
818         private RemoteStoreTemplateSource(String path)
819         {
820             this.templatePath = path;
821         }
822         
823         private long lastModified()
824         {
825             try
826             {
827                 return RemoteStore.this.lastModified(templatePath);
828             }
829             catch (IOException e)
830             {
831                 return -1;
832             }
833         }
834         
835         private Reader getReader(String encoding)
836             throws IOException
837         {
838             Response res = getDocumentResponse(templatePath);
839             if (encoding == null || encoding.equals(res.getEncoding()))
840             {
841                 return new StringReader(res.getResponse());
842             }
843             else
844             {
845                 return new InputStreamReader(res.getResponseStream(), encoding);
846             }
847         }
848     }
849     
850     
851     /**
852      * Script Content - loads from a Remote Store.
853      * 
854      * TODO: implement caching of remotely loaded script content?
855      * 
856      * @author Kevin Roast
857      */
858     private class RemoteScriptContent implements ScriptContent
859     {
860         private String scriptPath;
861         
862         /**
863          * Constructor
864          * 
865          * @param path  Path to remote script content
866          */
867         private RemoteScriptContent(String path)
868         {
869             this.scriptPath = path;
870         }
871         
872         /**
873          * @see org.springframework.extensions.webscripts.ScriptContent#getPath()
874          */
875         public String getPath()
876         {
877             return getStorePath() + '/' + this.scriptPath;
878         }
879 
880         /**
881          * @see org.springframework.extensions.webscripts.ScriptContent#getPathDescription()
882          */
883         public String getPathDescription()
884         {
885             return getStorePath() + '/' + this.scriptPath + " loaded from endpoint: " + getEndpoint();
886         }
887         
888         /**
889          * @see org.springframework.extensions.webscripts.ScriptContent#getInputStream()
890          */
891         public InputStream getInputStream()
892         {
893             try
894             {
895                 return getDocumentResponse(scriptPath).getResponseStream();
896             }
897             catch (IOException e)
898             {
899                 throw new WebScriptsPlatformException("Unable to load script: " + scriptPath, e);
900             }
901         }
902 
903         /**
904          * @see org.springframework.extensions.webscripts.ScriptContent#getReader()
905          */
906         public Reader getReader()
907         {
908             try
909             {
910                 Response res = getDocumentResponse(scriptPath);
911                 if (res.getEncoding() != null)
912                 {
913                     return new InputStreamReader(res.getResponseStream(), res.getEncoding());
914                 }
915                 else
916                 {
917                     return new InputStreamReader(res.getResponseStream());
918                 }
919             }
920             catch (IOException e)
921             {
922                 throw new WebScriptsPlatformException("Unable to load script: " + scriptPath, e);
923             }
924         }
925         
926         /**
927          * @see org.springframework.extensions.webscripts.ScriptContent#isCachable()
928          */
929         public boolean isCachable()
930         {
931             return false;
932         }
933         
934         /**
935          * @see org.springframework.extensions.webscripts.ScriptContent#isSecure()
936          */
937         public boolean isSecure()
938         {
939             return false;
940         }
941     }
942     
943     /**
944      * Converts an array of strings containing the full store root-relative
945      * paths to AVM objects to an array of strings containing paths that are
946      * relative to the "web application root".
947      * 
948      * For a standard AVM store, the root reference continues to be the
949      * root of the store.
950      * 
951      * For a WCM store, the root reference becomes the web application root
952      * directory - i.e. www/avm_webapps/ROOT
953      * 
954      * Next, this method uses the getStorePath() method to compute the
955      * starting point for relative references.  The getStorePath() method
956      * could return a fixed starting location (i.e. /alfresco) - this is
957      * specified via the setPath() method.
958      * 
959      * In addition, if the setWebappPathPrefix() method is used, one can
960      * more finely control how paths will vary for WCM stores.
961      * 
962      * Consider the case where the store path is set to "/alfresco" and
963      * the webappPathPrefix is set to "/WEB-INF/classes".
964      * 
965      * For a standard AVM store, the relative root becomes:
966      *    /alfresco
967      *    
968      * For a WCM store, the relative root becomes:
969      *    www/avm_webapps/ROOT/WEB-INF/classes/alfresco
970      *  
971      * Note that the webappPathPrefix is only applied for WCM stores.
972      * 
973      * @param fullPaths
974      */
975     private void convertToRelativePaths(String[] fullPaths)
976     {
977         // incoming paths are full avm paths
978         //
979         // this might be: www/avm_webapps/<webappId>/<path> in the case of a full WCM store
980         // or it might be: /<path> in the case of an plain AVM store (sitestore)
981         //
982         // we now want to truncate these to be relative to the storePath which was provided
983         
984         // determine the truncation string
985         String truncationString = "";
986         if (this.getStoreId() != null && this.getWebappId() != null)
987         {
988             // if we have a web app id, then we're looking at a WCM store
989             truncationString = "www/avm_webapps/" + this.getWebappId();
990             if (this.getStorePath() != null && this.getStorePath().length() > 0)
991             {
992                 truncationString += this.getStorePath();
993             }
994         }
995         
996         // if we have a valid truncation string
997         if (truncationString != null && truncationString.length() > 0)
998         {
999             // walk the files and convert paths so as to be relative to store path root
1000             for (int i = 0; i < fullPaths.length; i++)
1001             {
1002                 // the full avm path to the object
1003                 // if this is a WCM store, this will be the full path starting from www/avm_webapps...etc
1004                 String fullPath = fullPaths[i];
1005                 
1006                 int x = fullPath.indexOf(truncationString);
1007                 if (x != -1)
1008                 {
1009                     fullPath = fullPath.substring(x + truncationString.length());
1010                 }
1011                 
1012                 if (!fullPath.startsWith("/"))
1013                 {
1014                     fullPath = "/" + fullPath;
1015                 }
1016                 
1017                 fullPaths[i] = fullPath;
1018             }
1019         }
1020     }
1021 }