001: /******************************************************************************
002: * JBoss, a division of Red Hat *
003: * Copyright 2006, Red Hat Middleware, LLC, and individual *
004: * contributors as indicated by the @authors tag. See the *
005: * copyright.txt in the distribution for a full listing of *
006: * individual contributors. *
007: * *
008: * This is free software; you can redistribute it and/or modify it *
009: * under the terms of the GNU Lesser General Public License as *
010: * published by the Free Software Foundation; either version 2.1 of *
011: * the License, or (at your option) any later version. *
012: * *
013: * This software is distributed in the hope that it will be useful, *
014: * but WITHOUT ANY WARRANTY; without even the implied warranty of *
015: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
016: * Lesser General Public License for more details. *
017: * *
018: * You should have received a copy of the GNU Lesser General Public *
019: * License along with this software; if not, write to the Free *
020: * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA *
021: * 02110-1301 USA, or see the FSF site: http://www.fsf.org. *
022: ******************************************************************************/package org.jboss.portal.cms.impl.jcr;
023:
024: import org.apache.log4j.Level;
025: import org.apache.log4j.Logger;
026: import org.jboss.cache.Version;
027: import org.jboss.portal.cms.CMS;
028: import org.jboss.portal.cms.CMSException;
029: import org.jboss.portal.cms.CMSMimeMappings;
030: import org.jboss.portal.cms.Command;
031: import org.jboss.portal.cms.CommandFactory;
032: import org.jboss.portal.cms.impl.ContentImpl;
033: import org.jboss.portal.cms.impl.FileImpl;
034: import org.jboss.portal.cms.impl.FolderImpl;
035: import org.jboss.portal.cms.impl.jcr.jackrabbit.JackrabbitJCRService;
036: import org.jboss.portal.cms.model.CMSUser;
037: import org.jboss.portal.cms.model.Content;
038: import org.jboss.portal.cms.model.File;
039: import org.jboss.portal.cms.model.Folder;
040: import org.jboss.portal.cms.util.RepositoryUtil;
041: import org.jboss.portal.cms.workflow.ApprovePublish;
042: import org.jboss.portal.cms.security.AuthorizationManager;
043: import org.jboss.portal.common.invocation.InterceptorStackFactory;
044: import org.jboss.portal.common.invocation.Invocation;
045: import org.jboss.portal.common.invocation.InvocationException;
046: import org.jboss.portal.common.invocation.InvocationHandler;
047: import org.jboss.portal.common.io.IOTools;
048: import org.jboss.portal.common.net.URLNavigator;
049: import org.jboss.portal.common.net.URLVisitor;
050: import org.jboss.portal.common.xml.XMLTools;
051: import org.jboss.portal.identity.User;
052: import org.jboss.portal.jems.as.JNDI;
053: import org.jboss.portal.jems.as.system.AbstractJBossService;
054: import org.jboss.util.StopWatch;
055: import org.w3c.dom.DOMImplementation;
056: import org.w3c.dom.Document;
057: import org.w3c.dom.Element;
058:
059: import javax.jcr.Repository;
060: import javax.jcr.Session;
061: import javax.transaction.xa.XAResource;
062: import javax.transaction.xa.Xid;
063: import java.io.ByteArrayOutputStream;
064: import java.io.InputStream;
065: import java.net.URL;
066: import java.util.Date;
067: import java.util.Iterator;
068: import java.util.LinkedList;
069: import java.util.Locale;
070:
071: /**
072: * @author <a href="mailto:roy@jboss.org">Roy Russo</a>
073: * @author <a href="mailto:julien@jboss.org">Julien Viet</a>
074: * @author <a href="mailto:theute@jboss.org">Thomas Heute</a>
075: * @author <a href="mailto:sohil.shah@jboss.com">Sohil Shah</a>
076: */
077: public class JCRCMS extends AbstractJBossService implements CMS {
078: private static Logger log = Logger.getLogger(JCRCMS.class);
079:
080: private JCRCommandFactory commandFactory;
081: private boolean doChecking;
082: private Locale defaultLocale;
083:
084: private JCRService jcr;
085:
086: private String defaultContentLocation;
087:
088: private String homeDir;
089: private String repositoryName;
090:
091: private InterceptorStackFactory stackFactory;
092:
093: private Element config;
094:
095: private AuthorizationManager authorizationManager;
096:
097: private ApprovePublish approvePublishWorkflow;
098:
099: private String jndiName;
100:
101: private JNDI.Binding jndiBinding;
102:
103: private InvocationHandler handler = new InvocationHandler() {
104: public Object invoke(Invocation invocation) throws Exception,
105: InvocationException {
106: JCRCommand cmd = (JCRCommand) invocation;
107: return cmd.execute();
108: }
109: };
110:
111: /** Used for storing the logged in user information */
112: protected static ThreadLocal userInfo = new ThreadLocal();
113:
114: public static ThreadLocal getUserInfo() {
115: return JCRCMS.userInfo;
116: }
117:
118: /** This is used to turnoff workflow triggering only for this particular request through the CMS commands */
119: protected static ThreadLocal turnOffWorkflow = new ThreadLocal();
120:
121: public static void turnOffWorkflow() {
122: turnOffWorkflow.set(new Boolean(true));
123: }
124:
125: public static void turnOnWorkflow() {
126: turnOffWorkflow.set(null);
127: }
128:
129: public JCRCMS() {
130: commandFactory = new JCRCommandFactory();
131: }
132:
133: public String getRepositoryName() {
134: return repositoryName;
135: }
136:
137: public void setRepositoryName(String repositoryName) {
138: this .repositoryName = repositoryName;
139: }
140:
141: public String getHomeDir() {
142: return homeDir;
143: }
144:
145: public void setHomeDir(String homeDir) {
146: this .homeDir = homeDir;
147: }
148:
149: public String getDefaultContentLocation() {
150: return defaultContentLocation;
151: }
152:
153: public void setDefaultContentLocation(String defaultContentLocation) {
154: this .defaultContentLocation = defaultContentLocation;
155: }
156:
157: public Element getConfig() {
158: return config;
159: }
160:
161: public void setConfig(Element config) {
162: this .config = config;
163: }
164:
165: public JCRService getJCR() {
166: return jcr;
167: }
168:
169: public String getDefaultLocale() {
170: return defaultLocale.getLanguage();
171: }
172:
173: public void setDefaultLocale(String defaultLocale) {
174: this .defaultLocale = new Locale(defaultLocale);
175: }
176:
177: public boolean getDoChecking() {
178: return doChecking;
179: }
180:
181: public void setDoChecking(boolean doChecking) {
182: this .doChecking = doChecking;
183: }
184:
185: /** @return the approvePublishWorkflow */
186: public ApprovePublish getApprovePublishWorkflow() {
187: return approvePublishWorkflow;
188: }
189:
190: /** @param approvePublishWorkflow the approvePublishWorkflow to set */
191: public void setApprovePublishWorkflow(
192: ApprovePublish approvePublishWorkflow) {
193: this .approvePublishWorkflow = approvePublishWorkflow;
194: }
195:
196: /**
197: *
198: * @return
199: */
200: public AuthorizationManager getAuthorizationManager() {
201: return authorizationManager;
202: }
203:
204: /**
205: *
206: * @param authorizationManager
207: */
208: public void setAuthorizationManager(
209: AuthorizationManager authorizationManager) {
210: this .authorizationManager = authorizationManager;
211: }
212:
213: /** @return */
214: public String getJNDIName() {
215: return this .jndiName;
216: }
217:
218: /** @param jndiName */
219: public void setJNDIName(String jndiName) {
220: this .jndiName = jndiName;
221: }
222:
223: /** CMS Start */
224: public void startService() throws Exception {
225: if (this .jndiName != null) {
226: jndiBinding = new JNDI.Binding(jndiName, this );
227: jndiBinding.bind();
228: }
229:
230: //check the version of jbosscache being run
231: String cacheVersion = Version.getVersionString(Version
232: .getVersionShort());
233: log.info("JBossCache Version=" + cacheVersion);
234:
235: // See how long it takes us to start up
236: StopWatch watch = new StopWatch(true);
237: log.info("Starting JCR CMS");
238: //addInterceptors();
239: startJCR();
240: watch.stop();
241: log.info("Started JCR CMS in: " + watch);
242: }
243:
244: /** Shuts down the repo and unregisters it */
245: public void stopService() {
246: if (jndiBinding != null) {
247: jndiBinding.unbind();
248: jndiBinding = null;
249: }
250: log.info("Stopping JCR CMS");
251: stopJCR();
252: // removeInterceptors();
253: }
254:
255: public void startJCR() throws Exception {
256: // Serialize the embedded configuration
257: DOMImplementation impl = config.getOwnerDocument()
258: .getImplementation();
259: Document doc = impl.createDocument("tmp", "tmp", null);
260: Element copy = (Element) doc.importNode(config, true);
261: doc.removeChild(doc.getDocumentElement());
262: doc.appendChild(copy);
263: String result = XMLTools.toString(doc);
264: log.debug("Jackrabbit configuration : " + result);
265:
266: //
267: JackrabbitJCRService jr = new JackrabbitJCRService();
268: jr.setHomeDir(homeDir);
269: jr.setConfig(result);
270: jr.setRepositoryName(repositoryName);
271: jr.start();
272: jcr = jr;
273:
274: // suppress loud jackrabbit logging
275: Logger.getLogger("org.apache.jackrabbit").setLevel(Level.ERROR);
276:
277: //
278: if (doChecking) {
279: // Check default content exists.
280: if (!contentExists()) {
281: createContent();
282: }
283: }
284: }
285:
286: public void stopJCR() {
287: jcr.stop();
288: jcr = null;
289: }
290:
291: /**
292: * Checks for existence of default CMS content.
293: *
294: * @return
295: * @throws Exception
296: */
297: public boolean contentExists() throws Exception {
298: Session session = null;
299: try {
300: session = jcr.login("anonid", "");
301: return session.itemExists("/default");
302: } finally {
303: RepositoryUtil.safeLogout(session);
304: }
305: }
306:
307: // /**
308: /** Loads content from sar and adds it to the repo. */
309: public void createContent() throws Exception {
310: log.info("Creating default CMS content.");
311:
312: // Get the content
313: URL root = Thread.currentThread().getContextClassLoader()
314: .getResource(defaultContentLocation);
315:
316: //make the user executing these to create the default content, an cms root user
317: //without this, the fine grained security won't allow the creation
318: //Get the cms root user to create this content
319: if (this .authorizationManager != null) {
320: User user = this .authorizationManager.getProvider()
321: .getRoot();
322: if (user != null) {
323: JCRCMS.getUserInfo().set(user);
324: }
325: }
326:
327: // Iterate over the content
328: URLVisitor visitor = new URLVisitor() {
329: // Handle the name atoms
330: LinkedList atoms = new LinkedList();
331:
332: public void startDir(URL url, String name) {
333: // Compute the uri for the folder
334: StringBuffer tmp = new StringBuffer();
335: Iterator iterator = atoms.iterator();
336: while (iterator.hasNext()) {
337: String atom = (String) iterator.next();
338: tmp.append("/").append(atom);
339: }
340: tmp.append("/").append(name);
341: String uri = tmp.toString();
342:
343: //
344: Folder folder = new FolderImpl();
345: folder.setCreationDate(new Date());
346: folder.setDescription(name);
347: folder.setTitle(name);
348: folder.setLastModified(new Date());
349: folder.setName(name);
350: folder.setBasePath(uri);
351:
352: // Save folder
353: log.info("Creating folder " + uri);
354: Command saveCMD = getCommandFactory()
355: .createFolderSaveCommand(folder);
356: try {
357: execute(saveCMD);
358: } catch (CMSException e) {
359: e.printStackTrace();
360: }
361:
362: atoms.addLast(name);
363: }
364:
365: public void endDir(URL url, String name) {
366: atoms.removeLast();
367: }
368:
369: public void file(URL url, String name) {
370: InputStream in = null;
371: try {
372: StringBuffer tmp = new StringBuffer();
373:
374: // Compute the uri of the file
375: Iterator iterator = atoms.iterator();
376: while (iterator.hasNext()) {
377: String atom = (String) iterator.next();
378: tmp.append("/").append(atom);
379: }
380: tmp.append("/").append(name);
381: String uri = tmp.toString();
382:
383: // Load the content of the file
384: ByteArrayOutputStream baos = new ByteArrayOutputStream();
385: InputStream urlin = null;
386: try {
387: urlin = url.openStream();
388: IOTools.copy(urlin, baos);
389: } finally {
390: IOTools.safeClose(urlin);
391: }
392:
393: //
394: log.info("Creating file " + uri);
395: File file = new FileImpl();
396: file.setBasePath(uri);
397: Content content = new ContentImpl();
398: content.setEncoding("UTF-8");
399: content.setTitle("JBoss Portal");
400: content.setDescription("JBoss Portal");
401: content.setBasePath(uri + "/" + getDefaultLocale());
402: content.setBytes(baos.toByteArray());
403: String fileExt = file.getBasePath().substring(
404: file.getBasePath().lastIndexOf(".") + 1,
405: file.getBasePath().length());
406: CMSMimeMappings mapper = new CMSMimeMappings();
407: if (mapper.getMimeType(fileExt) != null) {
408: content
409: .setMimeType(mapper
410: .getMimeType(fileExt));
411: } else {
412: content.setMimeType("application/octet-stream");
413: }
414: file.setContent(new Locale(getDefaultLocale()),
415: content);
416:
417: Command newFileCMD = getCommandFactory()
418: .createNewFileCommand(file, content);
419: JCRCMS.turnOffWorkflow();
420: execute(newFileCMD);
421:
422: } catch (Exception e) {
423: e.printStackTrace();
424: } finally {
425: IOTools.safeClose(in);
426: }
427: }
428: };
429:
430: //
431: URLNavigator.visit(root, visitor);
432: log.info("Default content created.");
433: }
434:
435: public CommandFactory getCommandFactory() {
436: return commandFactory;
437: }
438:
439: public Repository getRepository() {
440: return jcr.getRepository();
441: }
442:
443: public Object execute(Command cmd) throws CMSException {
444: org.apache.jackrabbit.core.XASession session = null;
445: try {
446: session = (org.apache.jackrabbit.core.XASession) jcr.login(
447: "anonid", "");
448: } catch (Exception e) {
449: throw new RuntimeException(e); // Fixme
450: }
451: // get XAResource
452: XAResource xares = session.getXAResource();
453:
454: // create dummy Xid
455: Xid xid = new Xid() {
456: public byte[] getBranchQualifier() {
457: return new byte[0];
458: }
459:
460: public int getFormatId() {
461: return 0;
462: }
463:
464: public byte[] getGlobalTransactionId() {
465: return new byte[0];
466: }
467: };
468:
469: Object obj = null;
470: boolean isClusterDelegatedRequest = false; //used to indicate this request is from another cluster node instead of the master node
471: boolean clusterWorkflowStatus = false;
472: try {
473: xares.start(xid, XAResource.TMNOFLAGS);
474:
475: //Check and make sure in the case of a clustered call, the Identity propagated
476: //as part of the invocation is handled correctly
477: JCRCommandContext propagatedContext = (JCRCommandContext) ((JCRCommand) cmd)
478: .getContext();
479: if (propagatedContext != null) {
480: CMSUser propagatedUser = (CMSUser) propagatedContext
481: .getClusterContextInfo("user");
482: if (propagatedUser != null) {
483: JCRCMS.getUserInfo().set(propagatedUser);
484: isClusterDelegatedRequest = true;
485: }
486: Boolean workflowStatus = (Boolean) propagatedContext
487: .getClusterContextInfo("workflowStatus");
488: if (workflowStatus != null) {
489: JCRCMS.turnOffWorkflow();
490: clusterWorkflowStatus = true;
491: }
492: }
493:
494: // .... add new nodes & properties and save them
495: JCRCommand jcrCmd = (JCRCommand) cmd;
496: JCRCommandContext ctx = new JCRCommandContext(session,
497: commandFactory, defaultLocale);
498: jcrCmd.setContext(ctx);
499:
500: ctx.setAttribute(JCRCommandContext.scope, "user", JCRCMS
501: .getUserInfo().get());
502: Object isWorkflowOff = JCRCMS.turnOffWorkflow.get();
503: if (this .approvePublishWorkflow != null //this checks and makes sure workflow is activated for the CMS
504: && isWorkflowOff == null //this checks and makes sure workflow is not turned off only for this particular request
505: ) {
506: ctx.setAttribute(JCRCommandContext.scope,
507: "approvePublishWorkflow",
508: this .approvePublishWorkflow);
509: }
510:
511: if ((stackFactory != null)
512: && (stackFactory.getInterceptorStack().getLength() != 0)) {
513: jcrCmd.setHandler(handler);
514: obj = jcrCmd.invoke(stackFactory.getInterceptorStack());
515: jcrCmd.setHandler(null);
516: } else {
517: obj = jcrCmd.execute();
518: }
519:
520: //committ the transaction
521: xares.end(xid, XAResource.TMSUCCESS);
522: xares.prepare(xid);
523: session.save();
524: xares.commit(xid, false);
525: } catch (Exception e) {
526: e.printStackTrace();
527: try {
528: xares.rollback(xid);
529: } catch (Exception ex) {
530: //we tried to roll it back...not sure what more we can do here
531: throw new CMSException(ex);
532: }
533:
534: if (e instanceof CMSException) {
535: throw (CMSException) e;
536: } else if (e instanceof RuntimeException) {
537: throw (RuntimeException) e;
538: } else {
539: throw new CMSException(e);
540: }
541: } finally {
542: if (session != null) {
543: //must do this otherwise, the whole cms will hang
544: session.logout();
545: }
546: if (isClusterDelegatedRequest) {
547: JCRCMS.getUserInfo().set(null);
548: }
549: if (clusterWorkflowStatus) {
550: JCRCMS.turnOnWorkflow();
551: }
552: }
553: return obj;
554: }
555:
556: /** @return */
557: public boolean isWorkflowActivated() {
558: return (this .approvePublishWorkflow != null);
559: }
560:
561: public void setStackFactory(InterceptorStackFactory stackFactory) {
562: this .stackFactory = stackFactory;
563: }
564:
565: public InterceptorStackFactory getStackFactory() {
566: return stackFactory;
567: }
568: }
|