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: */
018: package org.apache.lenya.cms.workflow.usecases;
019:
020: import java.text.DateFormat;
021: import java.text.ParseException;
022: import java.text.SimpleDateFormat;
023: import java.util.ArrayList;
024: import java.util.Date;
025: import java.util.GregorianCalendar;
026: import java.util.HashSet;
027: import java.util.Iterator;
028: import java.util.List;
029: import java.util.Set;
030:
031: import org.apache.avalon.framework.service.ServiceManager;
032: import org.apache.avalon.framework.service.ServiceSelector;
033: import org.apache.cocoon.components.ContextHelper;
034: import org.apache.cocoon.environment.Request;
035: import org.apache.lenya.ac.AccessControlException;
036: import org.apache.lenya.ac.Identifiable;
037: import org.apache.lenya.ac.User;
038: import org.apache.lenya.cms.ac.PolicyUtil;
039: import org.apache.lenya.cms.linking.LinkManager;
040: import org.apache.lenya.cms.linking.LinkResolver;
041: import org.apache.lenya.cms.linking.LinkTarget;
042: import org.apache.lenya.cms.metadata.dublincore.DublinCoreHelper;
043: import org.apache.lenya.cms.observation.RepositoryEvent;
044: import org.apache.lenya.cms.observation.RepositoryEventFactory;
045: import org.apache.lenya.cms.publication.Document;
046: import org.apache.lenya.cms.publication.DocumentException;
047: import org.apache.lenya.cms.publication.DocumentFactory;
048: import org.apache.lenya.cms.publication.DocumentLocator;
049: import org.apache.lenya.cms.publication.DocumentManager;
050: import org.apache.lenya.cms.publication.Proxy;
051: import org.apache.lenya.cms.publication.Publication;
052: import org.apache.lenya.cms.publication.PublicationException;
053: import org.apache.lenya.cms.site.Link;
054: import org.apache.lenya.cms.site.SiteException;
055: import org.apache.lenya.cms.site.SiteManager;
056: import org.apache.lenya.cms.site.SiteNode;
057: import org.apache.lenya.cms.site.SiteStructure;
058: import org.apache.lenya.cms.usecase.UsecaseException;
059: import org.apache.lenya.cms.usecase.scheduling.UsecaseScheduler;
060: import org.apache.lenya.cms.workflow.WorkflowUtil;
061: import org.apache.lenya.cms.workflow.usecases.InvokeWorkflow;
062: import org.apache.lenya.notification.Message;
063: import org.apache.lenya.notification.NotificationEventDescriptor;
064: import org.apache.lenya.notification.NotificationException;
065: import org.apache.lenya.workflow.Version;
066: import org.apache.lenya.workflow.Workflowable;
067:
068: /**
069: * Publish usecase handler.
070: *
071: * @version $Id: Publish.java 595621 2007-11-16 10:56:39Z andreas $
072: */
073: public class Publish extends InvokeWorkflow {
074:
075: protected static final String MESSAGE_SUBJECT = "notification-message";
076: protected static final String MESSAGE_DOCUMENT_PUBLISHED = "document-published";
077: protected static final String SCHEDULE = "schedule";
078: protected static final String SCHEDULE_TIME = "schedule.time";
079: protected static final String SEND_NOTIFICATION = "sendNotification";
080: protected static final String UNPUBLISHED_LINKS = "unpublishedLinks";
081:
082: /**
083: * @see org.apache.lenya.cms.usecase.AbstractUsecase#initParameters()
084: */
085: protected void initParameters() {
086: super .initParameters();
087:
088: if (hasErrors() || getSourceDocument() == null) {
089: return;
090: }
091:
092: Date now = new GregorianCalendar().getTime();
093: DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
094: setParameter(SCHEDULE_TIME, format.format(now));
095:
096: setParameter(SEND_NOTIFICATION, Boolean.TRUE);
097:
098: setParameter(UNPUBLISHED_LINKS, new LinkList(this .manager,
099: getSourceDocument()));
100:
101: }
102:
103: protected boolean hasBrokenLinks() {
104: LinkManager linkMgr = null;
105: LinkResolver resolver = null;
106: try {
107: linkMgr = (LinkManager) this .manager
108: .lookup(LinkManager.ROLE);
109: resolver = (LinkResolver) this .manager
110: .lookup(LinkResolver.ROLE);
111: org.apache.lenya.cms.linking.Link[] links = linkMgr
112: .getLinksFrom(getSourceDocument());
113: for (int i = 0; i < links.length; i++) {
114: LinkTarget target = resolver.resolve(
115: getSourceDocument(), links[i].getUri());
116: if (!target.exists()) {
117: return true;
118: }
119: }
120: } catch (Exception e) {
121: throw new RuntimeException(e);
122: } finally {
123: if (linkMgr != null) {
124: this .manager.release(linkMgr);
125: }
126: if (resolver != null) {
127: this .manager.release(resolver);
128: }
129: }
130: return false;
131: }
132:
133: /**
134: * @see org.apache.lenya.cms.usecase.AbstractUsecase#getNodesToLock()
135: */
136: protected org.apache.lenya.cms.repository.Node[] getNodesToLock()
137: throws UsecaseException {
138: try {
139: List nodes = new ArrayList();
140:
141: Document doc = getSourceDocument();
142: if (doc != null) {
143: nodes.add(doc.getRepositoryNode());
144:
145: // lock the authoring site to avoid having live nodes for which no corresponding
146: // authoring node exists
147: nodes.add(doc.area().getSite().getRepositoryNode());
148:
149: // lock the live site to avoid overriding changes made by others
150: SiteStructure liveSite = doc.getPublication().getArea(
151: Publication.LIVE_AREA).getSite();
152: nodes.add(liveSite.getRepositoryNode());
153: }
154:
155: return (org.apache.lenya.cms.repository.Node[]) nodes
156: .toArray(new org.apache.lenya.cms.repository.Node[nodes
157: .size()]);
158:
159: } catch (Exception e) {
160: throw new UsecaseException(e);
161: }
162: }
163:
164: /**
165: * Checks if the workflow event is supported and the parent of the document
166: * exists in the live area.
167: *
168: * @see org.apache.lenya.cms.usecase.AbstractUsecase#doCheckPreconditions()
169: */
170: protected void doCheckPreconditions() throws Exception {
171: super .doCheckPreconditions();
172: if (!hasErrors()) {
173:
174: Document document = getSourceDocument();
175:
176: if (!document.getArea().equals(Publication.AUTHORING_AREA)) {
177: addErrorMessage("This usecase can only be invoked from the authoring area.");
178: return;
179: }
180:
181: Publication publication = document.getPublication();
182: DocumentFactory map = document.getFactory();
183: SiteStructure liveSite = publication.getArea(
184: Publication.LIVE_AREA).getSite();
185:
186: List missingDocuments = new ArrayList();
187:
188: ServiceSelector selector = null;
189: SiteManager siteManager = null;
190: try {
191: selector = (ServiceSelector) this .manager
192: .lookup(SiteManager.ROLE + "Selector");
193: siteManager = (SiteManager) selector.select(publication
194: .getSiteManagerHint());
195:
196: if (!liveSite.contains(document.getPath())) {
197: DocumentLocator liveLoc = document.getLocator()
198: .getAreaVersion(Publication.LIVE_AREA);
199: DocumentLocator[] requiredNodes = siteManager
200: .getRequiredResources(map, liveLoc);
201: for (int i = 0; i < requiredNodes.length; i++) {
202: String path = requiredNodes[i].getPath();
203: if (!liveSite.contains(path)) {
204: Link link = getExistingLink(path, document);
205: if (link != null) {
206: missingDocuments
207: .add(link.getDocument());
208: }
209: }
210:
211: }
212: }
213: } catch (Exception e) {
214: throw new RuntimeException(e);
215: } finally {
216: if (selector != null) {
217: if (siteManager != null) {
218: selector.release(siteManager);
219: }
220: this .manager.release(selector);
221: }
222: }
223:
224: if (!missingDocuments.isEmpty()) {
225: addErrorMessage("publish-missing-documents");
226: for (Iterator i = missingDocuments.iterator(); i
227: .hasNext();) {
228: Document doc = (Document) i.next();
229: /*
230: * This doesn't work yet, see
231: * https://issues.apache.org/jira/browse/COCOON-2057
232: * String[] params = { doc.getCanonicalWebappURL(),
233: * doc.getPath() + " (" + doc.getLanguage() + ")" };
234: */
235: String[] params = {
236: doc.getPath() + ":" + doc.getLanguage(),
237: DublinCoreHelper.getTitle(doc, true) };
238: addErrorMessage("missing-document", params);
239: }
240: }
241:
242: if (hasBrokenLinks()) {
243: addInfoMessage("publish-broken-links");
244: }
245: }
246: }
247:
248: /**
249: * Returns a link of a certain node, preferably in the document's language,
250: * or <code>null</code> if the node has no links.
251: * @param path The path of the node.
252: * @param document The document.
253: * @return A link or <code>null</code>.
254: * @throws SiteException if an error occurs.
255: */
256: protected Link getExistingLink(String path, Document document)
257: throws SiteException {
258: SiteNode node = document.area().getSite().getNode(path);
259: Link link = null;
260: String uuid = node.getUuid();
261: if (uuid != null && uuid.equals(document.getUUID())) {
262: if (node.hasLink(document.getLanguage())) {
263: link = node.getLink(document.getLanguage());
264: } else if (node.getLanguages().length > 0) {
265: link = node.getLink(node.getLanguages()[0]);
266: }
267: }
268: return link;
269: }
270:
271: protected void doCheckExecutionConditions() throws Exception {
272: super .doCheckExecutionConditions();
273: boolean schedule = Boolean.valueOf(
274: getBooleanCheckboxParameter(SCHEDULE)).booleanValue();
275: if (schedule) {
276: String dateString = getParameterAsString(SCHEDULE_TIME);
277: DateFormat format = new SimpleDateFormat(
278: "yyyy-MM-dd HH:mm:ss");
279: try {
280: format.parse(dateString);
281: } catch (ParseException e) {
282: addErrorMessage("scheduler-date-format-invalid");
283: }
284: }
285: }
286:
287: /**
288: * @see org.apache.lenya.cms.usecase.AbstractUsecase#doExecute()
289: */
290: protected void doExecute() throws Exception {
291:
292: boolean schedule = Boolean.valueOf(
293: getBooleanCheckboxParameter(SCHEDULE)).booleanValue();
294: if (schedule) {
295: deleteParameter(SCHEDULE);
296: String dateString = getParameterAsString(SCHEDULE_TIME);
297: DateFormat format = new SimpleDateFormat(
298: "yyyy-MM-dd HH:mm:ss");
299: UsecaseScheduler scheduler = null;
300: try {
301: Date date = format.parse(dateString);
302: scheduler = (UsecaseScheduler) this .manager
303: .lookup(UsecaseScheduler.ROLE);
304: scheduler.schedule(this , date);
305: } catch (ParseException e) {
306: addErrorMessage("scheduler-date-format-invalid");
307: } finally {
308: if (scheduler != null) {
309: this .manager.release(scheduler);
310: }
311: }
312: } else {
313: super .doExecute();
314: publish(getSourceDocument());
315: }
316: }
317:
318: protected void publish(Document authoringDocument)
319: throws DocumentException, SiteException,
320: PublicationException {
321:
322: createAncestorNodes(authoringDocument);
323:
324: DocumentManager documentManager = null;
325:
326: try {
327: documentManager = (DocumentManager) this .manager
328: .lookup(DocumentManager.ROLE);
329: documentManager.copyToArea(authoringDocument,
330: Publication.LIVE_AREA);
331:
332: boolean notify = Boolean.valueOf(
333: getBooleanCheckboxParameter(SEND_NOTIFICATION))
334: .booleanValue();
335: if (notify) {
336: sendNotification(authoringDocument);
337: }
338:
339: } catch (Exception e) {
340: throw new RuntimeException(e);
341: } finally {
342: if (documentManager != null) {
343: this .manager.release(documentManager);
344: }
345: }
346: }
347:
348: protected void sendNotification(Document authoringDocument)
349: throws NotificationException, DocumentException,
350: AccessControlException {
351: User sender = getSession().getIdentity().getUser();
352:
353: Workflowable workflowable = WorkflowUtil.getWorkflowable(
354: this .manager, getSession(), getLogger(),
355: authoringDocument);
356: Version versions[] = workflowable.getVersions();
357: Version version = versions[versions.length - 2];
358:
359: // we assume that the document has been submitted, otherwise we do
360: // nothing
361: if (version.getEvent().equals("submit")) {
362:
363: String userId = version.getUserId();
364: User user = PolicyUtil.getUser(this .manager,
365: authoringDocument.getCanonicalWebappURL(), userId,
366: getLogger());
367:
368: Identifiable[] recipients = { user };
369:
370: Document liveVersion = authoringDocument
371: .getAreaVersion(Publication.LIVE_AREA);
372: String url;
373:
374: Proxy proxy = liveVersion.getPublication().getProxy(
375: liveVersion, false);
376: if (proxy != null) {
377: url = proxy.getURL(liveVersion);
378: } else {
379: Request request = ContextHelper
380: .getRequest(this .context);
381: final String serverUrl = "http://"
382: + request.getServerName() + ":"
383: + request.getServerPort();
384: final String webappUrl = liveVersion
385: .getCanonicalWebappURL();
386: url = serverUrl + request.getContextPath() + webappUrl;
387: }
388: String[] params = { url };
389: Message message = new Message(MESSAGE_SUBJECT,
390: new String[0], MESSAGE_DOCUMENT_PUBLISHED, params,
391: sender, recipients);
392:
393: NotificationEventDescriptor descriptor = new NotificationEventDescriptor(
394: message);
395: RepositoryEvent event = RepositoryEventFactory
396: .createEvent(this .manager, getSession(),
397: getLogger(), descriptor);
398: getSession().enqueueEvent(event);
399: }
400: }
401:
402: protected void createAncestorNodes(Document document)
403: throws PublicationException, DocumentException,
404: SiteException {
405: SiteStructure liveSite = document.getPublication().getArea(
406: Publication.LIVE_AREA).getSite();
407: String[] steps = document.getPath().substring(1).split("/");
408: int s = 0;
409: String path = "";
410: while (s < steps.length) {
411: if (!liveSite.contains(path)) {
412: liveSite.add(path);
413: }
414: path += "/" + steps[s];
415: s++;
416: }
417: }
418:
419: /**
420: * A list of links originating from a document. Allows lazy loading rom the usecase view.
421: */
422: public static class LinkList {
423:
424: private Document document;
425: private Document[] documents;
426: private ServiceManager manager;
427:
428: /**
429: * @param manager The manager.
430: * @param doc The document to resolve the links from.
431: */
432: public LinkList(ServiceManager manager, Document doc) {
433: this .manager = manager;
434: this .document = doc;
435: }
436:
437: /**
438: * @return The link documents.
439: */
440: public Document[] getDocuments() {
441: if (this .documents == null) {
442: this .documents = getUnpublishedLinks();
443: }
444: return this .documents;
445: }
446:
447: protected Document[] getUnpublishedLinks() {
448: Set docs = new HashSet();
449: LinkManager linkMgr = null;
450: LinkResolver resolver = null;
451: try {
452: linkMgr = (LinkManager) this .manager
453: .lookup(LinkManager.ROLE);
454: resolver = (LinkResolver) this .manager
455: .lookup(LinkResolver.ROLE);
456: org.apache.lenya.cms.linking.Link[] links = linkMgr
457: .getLinksFrom(this .document);
458: for (int i = 0; i < links.length; i++) {
459: LinkTarget target = resolver.resolve(this .document,
460: links[i].getUri());
461: if (target.exists()) {
462: Document doc = target.getDocument();
463: if (!doc
464: .existsAreaVersion(Publication.LIVE_AREA)) {
465: docs.add(doc);
466: }
467: }
468: }
469: } catch (Exception e) {
470: throw new RuntimeException(e);
471: } finally {
472: if (linkMgr != null) {
473: this .manager.release(linkMgr);
474: }
475: if (resolver != null) {
476: this .manager.release(resolver);
477: }
478: }
479: return (Document[]) docs.toArray(new Document[docs.size()]);
480: }
481:
482: }
483:
484: }
|