View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    * 
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   * 
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.jetspeed.util.descriptor;
18  
19  import java.io.BufferedInputStream;
20  import java.io.File;
21  import java.io.FileFilter;
22  import java.io.FileInputStream;
23  import java.io.FileNotFoundException;
24  import java.io.FileOutputStream;
25  import java.io.FileWriter;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.io.InputStreamReader;
29  import java.io.OutputStream;
30  import java.io.OutputStreamWriter;
31  import java.io.Reader;
32  import java.io.UnsupportedEncodingException;
33  import java.io.Writer;
34  import java.net.MalformedURLException;
35  import java.net.URL;
36  import java.net.URLClassLoader;
37  import java.util.ArrayList;
38  import java.util.Collection;
39  import java.util.Iterator;
40  import java.util.List;
41  
42  import org.apache.commons.logging.Log;
43  import org.apache.commons.logging.LogFactory;
44  import org.apache.jetspeed.Jetspeed;
45  import org.apache.jetspeed.om.common.portlet.MutablePortletApplication;
46  import org.apache.jetspeed.om.common.servlet.MutableWebApplication;
47  import org.apache.jetspeed.tools.deploy.JetspeedWebApplicationRewriter;
48  import org.apache.jetspeed.tools.deploy.JetspeedWebApplicationRewriterFactory;
49  import org.apache.jetspeed.tools.pamanager.PortletApplicationException;
50  import org.apache.jetspeed.util.DirectoryHelper;
51  import org.apache.jetspeed.util.FileSystemHelper;
52  import org.apache.jetspeed.util.MultiFileChecksumHelper;
53  import org.apache.pluto.om.common.SecurityRoleRef;
54  import org.apache.pluto.om.common.SecurityRoleRefSet;
55  import org.apache.pluto.om.common.SecurityRoleSet;
56  import org.apache.pluto.om.portlet.PortletDefinition;
57  import org.jdom.Document;
58  import org.jdom.input.SAXBuilder;
59  import org.jdom.output.Format;
60  import org.jdom.output.XMLOutputter;
61  import org.xml.sax.EntityResolver;
62  import org.xml.sax.InputSource;
63  import org.xml.sax.SAXException;
64  
65  /***
66   * <p>
67   * This class facilitates operations a portlet applications WAR file or WAR
68   * file-like structure.
69   * </p>
70   * <p>
71   * This class is utility class used mainly implementors of
72   * {@link org.apache.jetspeed.pamanager.Deployment}and
73   * {@link org.apache.jetspeed.pamanager.Registration}to assist in deployment
74   * and undeployment of portlet applications.
75   * 
76   * @author <a href="mailto:sweaver@einnovation.com">Scott T. Weaver </a>
77   * @author <a href="mailto:mavery@einnovation.com">Matt Avery </a>
78   * @version $Id: PortletApplicationWar.java,v 1.10 2004/07/06 16:56:19 weaver
79   *          Exp $
80   */
81  public class PortletApplicationWar
82  {
83      protected static final String WEB_XML_STRING = 
84              "<?xml version='1.0' encoding='ISO-8859-1'?>" +
85              "<!DOCTYPE web-app " +
86              "PUBLIC '-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN' " + 
87             "'http://java.sun.com/dtd/web-app_2_3.dtd'>\n" +
88              "<web-app></web-app>";
89  
90      public static final String PORTLET_XML_PATH = "WEB-INF/portlet.xml";
91      public static final String WEB_XML_PATH = "WEB-INF/web.xml";
92      public static final String EXTENDED_PORTLET_XML_PATH = "WEB-INF/jetspeed-portlet.xml";
93  
94      protected static final int MAX_BUFFER_SIZE = 1024;
95  
96      public static final String JETSPEED_SERVLET_XPATH = "/web-app/servlet/servlet-name[contains(child::text(), \"JetspeedContainer\")]";
97      public static final String JETSPEED_SERVLET_MAPPING_XPATH = "/web-app/servlet-mapping/servlet-name[contains(child::text(), \"JetspeedContainer\")]";
98  
99      protected static final Log log = LogFactory.getLog("deployment");
100 
101     protected String paName;
102     protected String webAppContextRoot;
103     protected FileSystemHelper warStruct;
104     private MutableWebApplication webApp;
105     private MutablePortletApplication portletApp;
106     private long paChecksum;
107     protected final List openedResources;
108 
109     protected static final String[] ELEMENTS_BEFORE_SERVLET = new String[]{"icon", "display-name", "description",
110             "distributable", "context-param", "filter", "filter-mapping", "listener", "servlet"};
111     protected static final String[] ELEMENTS_BEFORE_SERVLET_MAPPING = new String[]{"icon", "display-name",
112             "description", "distributable", "context-param", "filter", "filter-mapping", "listener", "servlet",
113             "servlet-mapping"};
114 
115     /***
116      * @param warFile
117      *            {@link org.apache.jetspeed.util.FileSystemHelper}representing
118      *            the WAR file we are working with. This
119      *            <code>FileSystemHelper</code> can be an actual WAR file or a
120      *            directory structure layed out in a WAR-like fashion. name of
121      *            the portlet application the <code>warPath</code> contains
122      * @param webAppContextRoot
123      *            context root relative to the servlet container of this app
124      */
125     public PortletApplicationWar( FileSystemHelper warStruct, String paName, String webAppContextRoot )
126     {
127         this(warStruct, paName, webAppContextRoot, 0);
128     }
129 
130     public PortletApplicationWar( FileSystemHelper warStruct, String paName, String webAppContextRoot, long paChecksum )
131     {
132         validatePortletApplicationName(paName);
133 
134         this.paName = paName;
135         this.webAppContextRoot = webAppContextRoot;
136         this.openedResources = new ArrayList();
137         this.warStruct = warStruct;
138         this.paChecksum = paChecksum;
139     }
140     
141     public long getPortletApplicationChecksum() throws IOException
142     {
143         if ( this.paChecksum == 0)
144         {
145             this.paChecksum = MultiFileChecksumHelper.getChecksum(new File[] {
146                     new File(warStruct.getRootDirectory(), WEB_XML_PATH),
147                     new File(warStruct.getRootDirectory(), PORTLET_XML_PATH),
148                     new File(warStruct.getRootDirectory(), EXTENDED_PORTLET_XML_PATH) });
149         }
150         if (this.paChecksum == 0)
151         {
152           throw new IOException("Cannot find any deployment descriptor for Portlet Application "+paName);
153         }
154       return paChecksum;
155     }
156 
157     /***
158      * <p>
159      * validatePortletApplicationName
160      * </p>
161      * 
162      * @param paName
163      */
164     private void validatePortletApplicationName( String paName )
165     {
166         if (paName == null || paName.startsWith("/") || paName.startsWith("//") || paName.endsWith("/")
167                 || paName.endsWith("//"))
168         {
169             throw new IllegalStateException("Invalid paName \"" + paName
170                     + "\".  paName cannot be null nor can it begin nor end with any slashes.");
171         }
172     }
173 
174     /***
175      * 
176      * <p>
177      * createWebApp
178      * </p>
179      * Creates a web applicaiton object based on the values in this WAR's
180      * WEB-INF/web.xml
181      * 
182      * @return @throws
183      *         PortletApplicationException
184      * @throws IOException
185      * @see org.apache.jetspeed.util.descriptor.WebApplicationDescriptor
186      */
187     public MutableWebApplication createWebApp() throws PortletApplicationException, IOException
188     {
189         Reader webXmlReader = getReader(WEB_XML_PATH);
190 
191         try
192         {
193             WebApplicationDescriptor webAppDescriptor = new WebApplicationDescriptor(webXmlReader, webAppContextRoot);
194             webApp = webAppDescriptor.createWebApplication();
195             return webApp;
196         }
197 
198         finally
199         {
200             try
201             {
202                 if (webXmlReader != null)
203                 {
204                     webXmlReader.close();
205                 }
206             }
207             catch (IOException e1)
208             {
209                 e1.printStackTrace();
210             }
211         }
212 
213     }
214 
215     /***
216      * 
217      * <p>
218      * createPortletApp
219      * </p>
220      * Creates a portlet application object based of the WAR file's
221      * WEB-INF/portlet.xml
222      * 
223      * @return @throws
224      *         PortletApplicationException
225      * @throws IOException
226      * @see org.apache.jetspeed.uitl.descriptor.PortletApplicationDescriptor
227      */
228     public MutablePortletApplication createPortletApp(ClassLoader classLoader) throws PortletApplicationException, IOException
229     {
230         Reader portletXmlReader = getReader(PORTLET_XML_PATH);
231         
232         try
233         {
234             PortletApplicationDescriptor paDescriptor = new PortletApplicationDescriptor(portletXmlReader, paName);
235             portletApp = paDescriptor.createPortletApplication(classLoader);
236             // validate(portletApplication);
237             Reader extMetaDataXml = null;
238             try
239             {
240                 extMetaDataXml = getReader(EXTENDED_PORTLET_XML_PATH);
241                 if (extMetaDataXml != null)
242                 {
243                     ExtendedPortletMetadata extMetaData = new ExtendedPortletMetadata(extMetaDataXml, portletApp);
244                     extMetaData.load();
245                 }
246             }
247             catch (IOException e)
248             {
249                 if ( e instanceof FileNotFoundException )
250                 {
251                     log.info("No extended metadata found.");
252                 }
253                 else
254                 {
255                     throw new PortletApplicationException("Failed to load existing metadata.",e);
256                 }
257             }
258             catch (MetaDataException e)
259             {
260                 throw new PortletApplicationException("Failed to load existing metadata.", e);
261             }
262             finally
263             {
264                 if (null != extMetaDataXml)
265                 {
266                     extMetaDataXml.close();
267                 }
268             }
269             portletApp.setChecksum(paChecksum);
270             return portletApp;
271         }
272         finally
273         {
274             if (portletXmlReader != null)
275             {
276                 portletXmlReader.close();
277             }
278         }
279     }
280 
281     public MutablePortletApplication createPortletApp() 
282     throws PortletApplicationException, IOException
283     {
284         return createPortletApp(this.getClass().getClassLoader());
285     }
286     
287     /***
288      * 
289      * <p>
290      * getReader
291      * </p>
292      * Returns a <code>java.io.Reader</code> to a resource within this WAR's
293      * structure.
294      * 
295      * @param path
296      *            realtive to an object within this WAR's file structure
297      * @return java.io.Reader to the file within the WAR
298      * @throws IOException
299      *             if the path does not exist or there was a problem reading the
300      *             WAR.
301      *  
302      */
303     protected Reader getReader( String path ) throws IOException
304     {
305         BufferedInputStream is = new BufferedInputStream(getInputStream(path));
306 
307         String enc = "UTF-8";
308         try
309         {
310             is.mark(MAX_BUFFER_SIZE);
311             byte[] buf = new byte[MAX_BUFFER_SIZE];
312             int size = is.read(buf, 0, MAX_BUFFER_SIZE);
313             if (size > 0)
314             {
315                 String key = "encoding=\"";
316                 String data = new String(buf, 0, size, "US-ASCII");
317                 int lb = data.indexOf("\n");
318                 if (lb > 0)
319                 {
320                     data = data.substring(0, lb);
321                 }
322                 int off = data.indexOf(key);
323                 if (off > 0)
324                 {
325                     enc = data.substring(off + key.length(), data.indexOf('"', off + key.length()));
326                 }
327             }
328         }
329         catch (UnsupportedEncodingException e)
330         {
331             log.warn("Unsupported encoding.", e);
332         }
333         catch (IOException e)
334         {
335             log.warn("Unsupported encoding.", e);
336         }
337 
338         //Reset the bytes read
339         is.reset();
340         return new InputStreamReader(is, enc);
341     }
342 
343     /***
344      * 
345      * <p>
346      * getInputStream
347      * </p>
348      * 
349      * Returns a <code>java.io.InputStream</code> to a resource within this
350      * WAR's structure.
351      * 
352      * @param path
353      *            realtive to an object within this WAR's file structure
354      * @return java.io.InputStream to the file within the WAR
355      * @throws IOException
356      *             if the path does not exist or there was a problem reading the
357      *             WAR.
358      */
359     protected InputStream getInputStream( String path ) throws IOException
360     {
361         File child = new File(warStruct.getRootDirectory(), path);
362         if (child == null || !child.exists())
363         {
364             throw new FileNotFoundException("Unable to locate file or path " + child);
365         }
366 
367         FileInputStream fileInputStream = new FileInputStream(child);
368         openedResources.add(fileInputStream);
369         return fileInputStream;
370     }
371 
372     /***
373      * 
374      * <p>
375      * getOutputStream
376      * </p>
377      * 
378      * Returns a <code>java.io.OutputStream</code> to a resource within this
379      * WAR's structure.
380      * 
381      * @param path
382      *            realtive to an object within this WAR's file structure
383      * @return java.io.Reader to the file within the WAR
384      * @throws IOException
385      *             if the path does not exist or there was a problem reading the
386      *             WAR.
387      */
388     protected OutputStream getOutputStream( String path ) throws IOException
389     {
390         File child = new File(warStruct.getRootDirectory(), path);
391         if (child == null)             
392         {
393             throw new FileNotFoundException("Unable to locate file or path " + child);
394         }
395         FileOutputStream fileOutputStream = new FileOutputStream(child);
396         openedResources.add(fileOutputStream);
397         return fileOutputStream;
398     }
399 
400     protected Writer getWriter( String path ) throws IOException
401     {
402         return new OutputStreamWriter(getOutputStream(path));
403     }
404 
405     /***
406      * 
407      * <p>
408      * copyWar
409      * </p>
410      * Copies the entire WAR structure to the path defined in
411      * <code>targetAppRoot</code>
412      * 
413      * @param targetAppRoot
414      *            target to copy this WAR's content to. If the path ends in
415      *            <code>.war</code> or <code>.jar</code>. The war will be
416      *            copied into that file in jar format.
417      * @return PortletApplicationWar representing the newly created WAR.
418      * @throws IOException
419      */
420     public PortletApplicationWar copyWar( String targetAppRoot ) throws IOException
421     {
422         // FileObject target = fsManager.resolveFile(new
423         // File(targetAppRoot).getAbsolutePath());
424         FileSystemHelper target = new DirectoryHelper(new File(targetAppRoot));
425         try
426         {
427             target.copyFrom(warStruct.getRootDirectory());
428 
429             return new PortletApplicationWar(target, paName, webAppContextRoot, paChecksum);
430 
431         }
432         catch (IOException e)
433         {
434             throw e;
435         }
436         finally
437         {
438             target.close();
439 
440         }
441     }
442 
443     /***
444      * 
445      * <p>
446      * removeWar
447      * </p>
448      * Deletes this WAR. If the WAR is a file structure and not an actual WAR
449      * file, all children are delted first, then the directory is removed.
450      * 
451      * @throws IOException
452      *             if there is an error removing the WAR from the file system.
453      */
454     public void removeWar() throws IOException
455     {
456         if (warStruct.getRootDirectory().exists())
457         {
458             warStruct.remove();
459         }
460         else
461         {
462             throw new FileNotFoundException("PortletApplicationWar ," + warStruct.getRootDirectory()
463                     + ", does not exist.");
464         }
465     }
466 
467     /***
468      * Validate a PortletApplicationDefinition tree AFTER its
469      * WebApplicationDefinition has been loaded. Currently, only the security
470      * role references of the portlet definitions are validated:
471      * <ul>
472      * <li>A security role reference should reference a security role through a
473      * roleLink. A warning message is logged if a direct reference is used.
474      * <li>For a security role reference a security role must be defined in the
475      * web application. An error message is logged and a
476      * PortletApplicationException is thrown if not.
477      * </ul>
478      * 
479      * @throws PortletApplicationException
480      */
481     public void validate() throws PortletApplicationException
482     {
483         if (portletApp == null || webApp == null)
484         {
485             throw new IllegalStateException(
486                     "createWebApp() and createPortletApp() must be called before invoking validate()");
487         }
488 
489         SecurityRoleSet roles = webApp.getSecurityRoles();
490         Collection portlets = portletApp.getPortletDefinitions();
491         Iterator portletIterator = portlets.iterator();
492         while (portletIterator.hasNext())
493         {
494             PortletDefinition portlet = (PortletDefinition) portletIterator.next();
495             SecurityRoleRefSet securityRoleRefs = portlet.getInitSecurityRoleRefSet();
496             Iterator roleRefsIterator = securityRoleRefs.iterator();
497             while (roleRefsIterator.hasNext())
498             {
499                 SecurityRoleRef roleRef = (SecurityRoleRef) roleRefsIterator.next();
500                 String roleName = roleRef.getRoleLink();
501                 if (roleName == null || roleName.length() == 0)
502                 {
503                     roleName = roleRef.getRoleName();
504                 }
505                 if (roles.get(roleName) == null)
506                 {
507                     String errorMsg = "Undefined security role " + roleName + " referenced from portlet "
508                             + portlet.getName();
509                     throw new PortletApplicationException(errorMsg);
510                 }
511             }
512         }
513     }
514 
515     /***
516      * 
517      * <p>
518      * processWebXML
519      * </p>
520      * 
521      * Infuses this PortletApplicationWar's web.xml file with
522      * <code>servlet</code> and a <code>servlet-mapping</code> element for
523      * the JetspeedContainer servlet. This is only done if the descriptor does
524      * not already contain these items.
525      * 
526      * @throws MetaDataException
527      *             if there is a problem infusing
528      */
529     public void processWebXML() throws MetaDataException
530     {
531         SAXBuilder builder = new SAXBuilder();
532         Writer webXmlWriter = null;
533         InputStream webXmlIn = null;
534 
535         try
536         {
537             // Use the local dtd instead of remote dtd. This
538             // allows to deploy the application offline
539             builder.setEntityResolver(new EntityResolver()
540             {
541                 public InputSource resolveEntity( java.lang.String publicId, java.lang.String systemId )
542                         throws SAXException, java.io.IOException
543                 {
544 
545                     if (systemId.equals("http://java.sun.com/dtd/web-app_2_3.dtd"))
546                     {
547                         return new InputSource(getClass().getResourceAsStream("web-app_2_3.dtd"));
548                     }
549                     else return null;
550                 }
551             });
552 
553             Document doc = null;
554             
555             try
556             {
557                 webXmlIn = getInputStream(WEB_XML_PATH);
558                 doc = builder.build(webXmlIn);
559             }
560             catch (FileNotFoundException fnfe)
561             {
562                 // web.xml does not exist, create it
563                 File file = File.createTempFile("j2-temp-", ".xml");
564                 FileWriter writer = new FileWriter(file);
565                 writer.write(WEB_XML_STRING);
566                 writer.close();
567                 doc = builder.build(file);
568                 file.delete();
569             }
570             
571             
572             if (webXmlIn != null)
573             {
574                 webXmlIn.close();
575             }
576 
577             JetspeedWebApplicationRewriterFactory rewriterFactory = new JetspeedWebApplicationRewriterFactory();
578             JetspeedWebApplicationRewriter rewriter = rewriterFactory.getInstance(doc);
579             rewriter.processWebXML();
580             
581             if (rewriter.isChanged())
582             {
583                 System.out.println("Writing out infused web.xml for " + paName);
584                 XMLOutputter output = new XMLOutputter(Format.getPrettyFormat());
585                 webXmlWriter = getWriter(WEB_XML_PATH);
586                 output.output(doc, webXmlWriter);
587                 webXmlWriter.flush();
588 
589             }
590             
591             if(rewriter.isPortletTaglibAdded())
592             {
593                 //add portlet tag lib to war
594                 String path = Jetspeed.getRealPath("WEB-INF/tld");
595                 if (path != null)
596                 {
597                     File portletTaglibDir = new File(path);
598                     File child = new File(warStruct.getRootDirectory(), "WEB-INF/tld");
599                     DirectoryHelper dh = new DirectoryHelper(child);
600                     dh.copyFrom(portletTaglibDir, new FileFilter(){
601 
602                         public boolean accept(File pathname)
603                         {
604                             return pathname.getName().indexOf("portlet.tld") != -1;
605                         }                    
606                     });                
607                     dh.close();
608                 }
609             }
610 
611         }
612         catch (Exception e)
613         {
614             e.printStackTrace();
615             throw new MetaDataException("Unable to process web.xml for infusion " + e.toString(), e);
616         }
617         finally
618         {
619             if (webXmlWriter != null)
620             {
621                 try
622                 {
623                     webXmlWriter.close();
624                 }
625                 catch (IOException e1)
626                 {
627 
628                 }
629             }
630 
631             if (webXmlIn != null)
632             {
633                 try
634                 {
635                     webXmlIn.close();
636                 }
637                 catch (IOException e1)
638                 {
639 
640                 }
641             }
642         }
643 
644     }
645 
646 
647     /***
648      * 
649      * <p>
650      * close
651      * </p>
652      * Closes any resource this PortletApplicationWar may have opened.
653      * 
654      * @throws IOException
655      */
656     public void close() throws IOException
657     {
658 
659         Iterator resources = openedResources.iterator();
660         while (resources.hasNext())
661         {
662             try
663             {
664                 Object res = resources.next();
665                 if (res instanceof InputStream)
666                 {
667                     ((InputStream) res).close();
668                 }
669                 else if (res instanceof OutputStream)
670                 {
671                     ((OutputStream) res).close();
672                 }
673             }
674             catch (Exception e)
675             {
676 
677             }
678         }
679 
680     }
681 
682     /***
683      * 
684      * <p>
685      * createClassloader
686      * </p>
687      * 
688      * Use this method to create a classloader based on this wars structure.
689      * I.e. it will create a ClassLoader containing the contents of
690      * WEB-INF/classes and WEB-INF/lib and the ClassLoader will be searched in
691      * that order.
692      * 
693      * 
694      * @param parent
695      *            Parent ClassLoader. Can be <code>null</code>
696      * @return @throws
697      *         IOException
698      */
699     public ClassLoader createClassloader( ClassLoader parent ) throws IOException
700     {
701         ArrayList urls = new ArrayList();
702         File webInfClasses = null;
703 
704         webInfClasses = new File(warStruct.getRootDirectory(), ("WEB-INF/classes/"));
705         if (webInfClasses.exists())
706         {
707             log.info("Adding " + webInfClasses.toURL() + " to class path.");
708             urls.add(webInfClasses.toURL());
709         }
710 
711         File webInfLib = new File(warStruct.getRootDirectory(), "WEB-INF/lib");
712 
713         if (webInfLib.exists())
714         {
715             File[] jars = webInfLib.listFiles();
716 
717             for (int i = 0; i < jars.length; i++)
718             {
719                 File jar = jars[i];
720                 log.info("Adding " + jar.toURL() + " to class path.");
721                 urls.add(jar.toURL());
722             }
723         }
724 
725         return new URLClassLoader((URL[]) urls.toArray(new URL[urls.size()]), parent);
726     }
727 
728     /***
729      * @return Returns the paName.
730      */
731     public String getPortletApplicationName()
732     {
733         return paName;
734     }
735 
736     /***
737      * 
738      * <p>
739      * getDeployedPath
740      * </p>
741      * 
742      * @return A string representing the path to this WAR in the form of a URL
743      *         or <code>null</code> is the URL could not be created.
744      */
745     public String getDeployedPath()
746     {
747         try
748         {
749             return warStruct.getRootDirectory().toURL().toExternalForm();
750         }
751         catch (MalformedURLException e)
752         {
753             return null;
754         }
755     }
756     
757     public FileSystemHelper getFileSystem()
758     {
759         return warStruct;
760     }
761 }