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