package com.psddev.cms.tool.page; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import javax.el.ELContext; import javax.el.ExpressionFactory; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; import javax.servlet.jsp.JspFactory; import javax.servlet.jsp.PageContext; import com.psddev.cms.db.Content; import com.psddev.cms.db.Directory; import com.psddev.cms.db.Draft; import com.psddev.cms.db.Overlay; import com.psddev.cms.db.Preview; import com.psddev.cms.db.Site; import com.psddev.cms.db.ToolUser; import com.psddev.cms.db.WorkInProgress; import com.psddev.cms.db.Workflow; import com.psddev.cms.db.WorkflowLog; import com.psddev.cms.tool.AuthenticationFilter; import com.psddev.cms.tool.CmsTool; import com.psddev.cms.tool.PageServlet; import com.psddev.cms.tool.ToolPageContext; import com.psddev.cms.tool.page.content.Edit; import com.psddev.dari.db.Database; import com.psddev.dari.db.ObjectField; import com.psddev.dari.db.ObjectType; import com.psddev.dari.db.PredicateParser; import com.psddev.dari.db.Query; import com.psddev.dari.db.Recordable; import com.psddev.dari.db.State; import com.psddev.dari.util.CompactMap; import com.psddev.dari.util.ErrorUtils; import com.psddev.dari.util.HtmlWriter; import com.psddev.dari.util.ObjectToIterable; import com.psddev.dari.util.ObjectUtils; import com.psddev.dari.util.RoutingFilter; import com.psddev.dari.util.Settings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @RoutingFilter.Path(application = "cms", value = "/contentState") public class ContentState extends PageServlet { private static final Logger LOGGER = LoggerFactory.getLogger(ContentState.class); private static final long serialVersionUID = 1L; @Override protected String getPermissionId() { return null; } @Override @SuppressWarnings("unchecked") protected void doService(ToolPageContext page) throws IOException, ServletException { Object object = page.findOrReserve(); if (object == null) { return; } Date wipCreateDate = new Date(Database.Static.getDefault().now()); // Pretend to update the object. State state = State.getInstance(object); String oldValuesString = page.param(String.class, state.getId() + "/oldValues"); Map<String, Object> oldValues = !ObjectUtils.isBlank(oldValuesString) ? (Map<String, Object>) ObjectUtils.fromJson(oldValuesString) : Draft.findOldValues(object); // Change the old values to include the draft differences so that // the change detection during draft edit work correctly. Draft draft = page.getOverlaidDraft(object); if (draft != null) { oldValues = Draft.mergeDifferences( state.getDatabase().getEnvironment(), oldValues, draft.getDifferences()); } // Change the old values to include the overlay differences so that // the change detection during overlay edit work correctly. Overlay overlay = Edit.getOverlay(object); if (overlay != null) { oldValues = Draft.mergeDifferences( state.getDatabase().getEnvironment(), oldValues, overlay.getDifferences()); } if (state.isNew() || object instanceof Draft || state.as(Content.ObjectModification.class).isDraft() || state.as(Workflow.Data.class).getCurrentState() != null) { page.setContentFormScheduleDate(object); } WorkflowLog log = null; try { state.beginWrites(); page.updateUsingParameters(object); Edit.updateUsingWidgets(page, object); UUID workflowLogId = page.param(UUID.class, "workflowLogId"); if (workflowLogId != null) { log = new WorkflowLog(); log.getState().setId(workflowLogId); page.updateUsingParameters(log); state.as(Workflow.Data.class).setCurrentLog(log); } page.publish(object); } catch (IOException error) { throw error; } catch (ServletException error) { throw error; } catch (RuntimeException error) { throw error; } catch (Exception error) { ErrorUtils.rethrow(error); } finally { state.endWrites(); } // Expensive operations that should only trigger occasionally. boolean idle = page.param(boolean.class, "idle"); ToolUser user = page.getUser(); if (idle) { boolean saveUser = false; // Automatically save newly created drafts when the user is idle. Content.ObjectModification contentData = state.as(Content.ObjectModification.class); if (idle && (state.isNew() || contentData.isDraft()) && !page.getCmsTool().isDisableAutomaticallySavingDrafts()) { contentData.setDraft(true); contentData.setUpdateDate(new Date()); contentData.setUpdateUser(user); state.save(); Set<UUID> automaticallySavedDraftIds = user.getAutomaticallySavedDraftIds(); UUID id = state.getId(); if (!automaticallySavedDraftIds.contains(id)) { saveUser = true; automaticallySavedDraftIds.add(id); } } // Preview for looking glass. Preview preview = new Preview(); UUID currentPreviewId = user.getCurrentPreviewId(); if (currentPreviewId == null) { saveUser = true; currentPreviewId = preview.getId(); user.setCurrentPreviewId(currentPreviewId); } Map<String, Object> values = state.getSimpleValues(); preview.getState().setId(currentPreviewId); preview.setCreateDate(new Date()); preview.setObjectType(state.getType()); preview.setObjectId(state.getId()); preview.setObjectValues(values); preview.setSite(page.getSite()); preview.save(); AuthenticationFilter.Static.setCurrentPreview(page.getRequest(), page.getResponse(), preview); user.saveAction(page.getRequest(), object); if (saveUser) { user.save(); } } Map<String, Object> jsonResponse = new CompactMap<String, Object>(); // Differences between existing and pending content. Map<String, Map<String, Object>> allDifferences = Draft.findDifferences( state.getDatabase().getEnvironment(), oldValues, state.getSimpleValues()); // Split differences that are visible and hidden in the UI. Map<String, Map<String, Object>> differences = new CompactMap<>(); Map<String, Map<String, Object>> hiddenDifferences = new CompactMap<>(); Map<String, List<String>> fieldNamesById = (Map<String, List<String>>) ObjectUtils.fromJson(page.param(String.class, "_fns")); if (fieldNamesById == null) { fieldNamesById = new CompactMap<>(); } for (Map.Entry<String, Map<String, Object>> entry : allDifferences.entrySet()) { String id = entry.getKey(); Map<String, Object> allValues = entry.getValue(); List<String> fieldNames = fieldNamesById.get(id); if (fieldNames == null) { hiddenDifferences.put(id, allValues); } else { Map<String, Object> values = new CompactMap<>(); Map<String, Object> hiddenValues = new CompactMap<>(); for (Map.Entry<String, Object> allValue : allValues.entrySet()) { String key = allValue.getKey(); (fieldNames.contains(key) ? values : hiddenValues).put(key, allValue.getValue()); } if (!values.isEmpty()) { differences.put(id, values); } if (!hiddenValues.isEmpty()) { hiddenDifferences.put(id, hiddenValues); } } } jsonResponse.put("_differences", differences); jsonResponse.put("_hiddenDifferences", hiddenDifferences); if (page.getOverlaidHistory(object) == null && page.getOverlaidDraft(object) == null && page.param(boolean.class, "wip") && !user.isDisableWorkInProgress() && !Query.from(CmsTool.class).first().isDisableWorkInProgress()) { ObjectType contentType = state.getType(); UUID contentId = state.getId(); WorkInProgress wip = Query.from(WorkInProgress.class) .where("owner = ?", user) .and("contentType = ?", contentType) .and("contentId = ?", contentId) .first(); if (differences.isEmpty()) { if (wip != null) { jsonResponse.put("_wip", page.localize(getClass(), "message.wipDeleted")); wip.delete(); } } else { if (wip == null) { wip = new WorkInProgress(); wip.setOwner(user); wip.setContentType(contentType); wip.setContentId(contentId); jsonResponse.put("_wip", page.localize(getClass(), "message.wipCreated")); } else { jsonResponse.put("_wip", page.localize(getClass(), "message.wipUpdated")); } wip.setContentLabel(state.getLabel()); wip.setCreateDate(wipCreateDate); wip.setUpdateDate(new Date(Database.Static.getDefault().now())); wip.setDifferences(differences); wip.save(); List<WorkInProgress> more = Query.from(WorkInProgress.class) .where("owner = ?", user) .and("updateDate != missing") .sortDescending("updateDate") .select(50, 1) .getItems(); if (!more.isEmpty()) { Query.from(WorkInProgress.class) .where("owner = ?", user) .and("updateDate < ?", more.get(0).getUpdateDate()) .deleteAll(); } } } // HTML display for the URL widget. @SuppressWarnings("unchecked") Set<Directory.Path> newPaths = (Set<Directory.Path>) state.getExtras().get("cms.newPaths"); if (!ObjectUtils.isBlank(newPaths)) { StringWriter string = new StringWriter(); @SuppressWarnings("resource") HtmlWriter html = new HtmlWriter(string); html.writeStart("ul"); for (Directory.Path p : newPaths) { Site s = p.getSite(); html.writeStart("li"); html.writeStart("a", "target", "_blank", "href", p.getPath()); html.writeHtml(p.getPath()); html.writeEnd(); html.writeHtml(" ("); if (s != null) { html.writeHtml(s.getLabel()); html.writeHtml(" - "); } html.writeHtml(p.getType()); html.writeHtml(")"); html.writeEnd(); } html.writeEnd(); jsonResponse.put("_urlWidgetHtml", string.toString()); } // Evaluate all dynamic texts. List<String> dynamicTexts = new ArrayList<String>(); List<String> dynamicPredicates = new ArrayList<>(); List<String> dynamicSearcherPaths = new ArrayList<>(); JspFactory jspFactory = JspFactory.getDefaultFactory(); PageContext pageContext = jspFactory.getPageContext(this, page.getRequest(), page.getResponse(), null, false, 0, false); try { ExpressionFactory expressionFactory = jspFactory.getJspApplicationContext(getServletContext()).getExpressionFactory(); ELContext elContext = pageContext.getELContext(); List<UUID> contentIds = page.params(UUID.class, "_dti"); int contentIdsSize = contentIds.size(); List<String> templates = page.params(String.class, "_dtt"); List<String> contentFieldNames = page.params(String.class, "_dtf"); List<String> predicates = page.params(String.class, "_dtq"); List<String> searcherPaths = page.params(String.class, "_dts"); int contentFieldNamesSize = contentFieldNames.size(); for (int i = 0, size = templates.size(); i < size; ++ i) { Object content = null; try { if (i < contentIdsSize) { UUID contentId = contentIds.get(i); content = log != null && log.getId().equals(contentId) ? log : findContent(object, contentId); } } catch (RuntimeException e) { // Ignore. } String dynamicText = ""; String dynamicPredicate = ""; String dynamicSearcherPath = ""; if (content != null) { try { pageContext.setAttribute("toolPageContext", page); pageContext.setAttribute("content", content); ObjectField field = null; String contentFieldName = i < contentFieldNamesSize ? contentFieldNames.get(i) : null; if (contentFieldName != null) { field = State.getInstance(content).getField(contentFieldName); } pageContext.setAttribute("field", field); dynamicText = ((String) expressionFactory.createValueExpression(elContext, templates.get(i), String.class).getValue(elContext)); } catch (RuntimeException error) { if (Settings.isProduction()) { LOGGER.warn("Could not generate dynamic text!", error); } else { StringWriter string = new StringWriter(); error.printStackTrace(new PrintWriter(string)); dynamicText = string.toString(); } } try { pageContext.setAttribute("toolPageContext", page); pageContext.setAttribute("content", content); ObjectField field = null; String contentFieldName = i < contentFieldNamesSize ? contentFieldNames.get(i) : null; if (contentFieldName != null) { field = State.getInstance(content).getField(contentFieldName); } pageContext.setAttribute("field", field); dynamicSearcherPath = ((String) expressionFactory.createValueExpression(elContext, searcherPaths.get(i), String.class).getValue(elContext)); } catch (RuntimeException error) { if (Settings.isProduction()) { LOGGER.warn("Could not generate dynamic text!", error); } else { StringWriter string = new StringWriter(); error.printStackTrace(new PrintWriter(string)); dynamicText = string.toString(); } } try { if (!ObjectUtils.isBlank(predicates.get(i))) { dynamicPredicate = PredicateParser.Static.parse(predicates.get(i), content).toString(); } } catch (RuntimeException error) { LOGGER.warn("Could not generate dynamic predicate!", error); } } dynamicTexts.add(dynamicText); dynamicPredicates.add(dynamicPredicate); dynamicSearcherPaths.add(dynamicSearcherPath); } } finally { jspFactory.releasePageContext(pageContext); } jsonResponse.put("_dynamicTexts", dynamicTexts); jsonResponse.put("_dynamicPredicates", dynamicPredicates); jsonResponse.put("_dynamicSearcherPaths", dynamicSearcherPaths); // Write the JSON response. HttpServletResponse response = page.getResponse(); response.setContentType("application/json"); page.write(ObjectUtils.toJson(jsonResponse)); } private Object findContent(Object object, UUID id) { if (id != null) { State state = State.getInstance(object); if (state.getId().equals(id)) { return object; } for (Map.Entry<String, Object> entry : state.entrySet()) { String name = entry.getKey(); Object value = entry.getValue(); ObjectField field = state.getField(name); Object found = findEmbedded(value, id, field != null && field.isEmbedded()); if (found != null) { return found; } } } return null; } private Object findEmbedded(Object value, UUID id, boolean embedded) { if (value != null) { if (value instanceof Recordable) { State valueState = ((Recordable) value).getState(); if (valueState.isNew()) { ObjectType type; if (embedded || ((type = valueState.getType()) != null && type.isEmbedded())) { Object found = findContent(value, id); if (found != null) { return found; } } } } else { Iterable<?> valueIterable = value instanceof Map ? ((Map<?, ?>) value).values() : ObjectToIterable.iterable(value); if (valueIterable != null) { for (Object item : valueIterable) { Object found = findEmbedded(item, id, embedded); if (found != null) { return found; } } } } } return null; } }