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;
}
}