package com.psddev.cms.tool.page.content;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.psddev.cms.db.Content;
import com.psddev.cms.db.Draft;
import com.psddev.cms.db.Overlay;
import com.psddev.cms.db.OverlayProvider;
import com.psddev.cms.db.Page;
import com.psddev.cms.db.ToolUi;
import com.psddev.cms.db.ToolUser;
import com.psddev.cms.db.WorkInProgress;
import com.psddev.cms.tool.CmsTool;
import com.psddev.cms.tool.ContentEditWidget;
import com.psddev.cms.tool.ContentEditWidgetDisplay;
import com.psddev.cms.tool.ContentEditWidgetPlacement;
import com.psddev.cms.tool.ContentEditWidgetFilter;
import com.psddev.cms.tool.Tool;
import com.psddev.cms.tool.ToolPageContext;
import com.psddev.cms.tool.UpdatingContentEditWidget;
import com.psddev.cms.tool.Widget;
import com.psddev.dari.db.ObjectField;
import com.psddev.dari.db.ObjectType;
import com.psddev.dari.db.Query;
import com.psddev.dari.db.State;
import com.psddev.dari.util.ClassFinder;
import com.psddev.dari.util.DependencyResolver;
import com.psddev.dari.util.HtmlWriter;
import com.psddev.dari.util.ObjectUtils;
import com.psddev.dari.util.TypeDefinition;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
public class Edit {
private static final String ATTRIBUTE_PREFIX = Edit.class.getName() + ".";
private static final String WIP_DIFFERENCE_IDS_ATTRIBUTE = ATTRIBUTE_PREFIX + "wipDifferenceIds";
public static Overlay getOverlay(Object content) {
return content != null
? (Overlay) State.getInstance(content).getExtras().get("cms.tool.overlay")
: null;
}
public static void writeOverlayProviderSelect(ToolPageContext page, Object content, OverlayProvider selected) throws IOException {
List<OverlayProvider> overlayProviders = Query.from(OverlayProvider.class).selectAll();
overlayProviders.removeIf(p -> !p.shouldOverlay(content));
if (overlayProviders.isEmpty()) {
return;
}
UUID contentId = State.getInstance(content).getId();
page.writeStart("div", "class", "OverlayProviderSelect");
page.writeStart("ul");
{
page.writeStart("li", "class", selected == null ? "selected" : null);
{
page.writeStart("a",
"href", page.url("",
"id", contentId,
"overlayId", null));
page.writeHtml("Default");
page.writeEnd();
}
page.writeEnd();
for (OverlayProvider overlayProvider : overlayProviders) {
page.writeStart("li", "class", overlayProvider.equals(selected) ? "selected" : null);
{
page.writeStart("a",
"href", page.url("",
"id", contentId,
"overlayId", overlayProvider.getState().getId()));
page.writeObjectLabel(overlayProvider);
page.writeEnd();
}
page.writeEnd();
}
}
page.writeEnd();
page.writeEnd();
}
/**
* Creates the placeholder text for the given {@code field} that should
* be displayed to the user in the context of the given {@code page}.
*
* @param page Can't be {@code null}.
* @param field Can't be {@code null}.
* @return Never {@code null}.
*/
public static String createPlaceholderText(ToolPageContext page, ObjectField field) throws IOException {
String placeholder = field.as(ToolUi.class).getPlaceholder();
if (field.isRequired()) {
String required = page.localize(field.getParentType(), "placeholder.required");
if (ObjectUtils.isBlank(placeholder)) {
placeholder = required;
} else {
placeholder += ' ';
placeholder += required;
}
}
if (ObjectUtils.isBlank(placeholder)) {
return "";
} else {
return placeholder;
}
}
/**
* Restores the work in progress associated with the given {@code content}
* in the context of the given {@code page}.
*
* <p>If successful, writes an appropriate message to the output attached
* to the given {@code page}.</p>
*
* @param page Can't be {@code null}.
* @param content Can't be {@code null}.
*/
public static void restoreWorkInProgress(ToolPageContext page, Object content) throws IOException {
if (page.getOverlaidHistory(content) != null
|| page.getOverlaidDraft(content) != null) {
return;
}
State state = State.getInstance(content);
if (state.hasAnyErrors()) {
return;
}
ToolUser user = page.getUser();
if (user.isDisableWorkInProgress()
|| page.getCmsTool().isDisableWorkInProgress()) {
return;
}
WorkInProgress wip = Query.from(WorkInProgress.class)
.where("owner = ?", user)
.and("contentId = ?", state.getId())
.first();
if (wip == null) {
return;
}
Date wipCreate = wip.getCreateDate();
Date wipUpdate = wip.getUpdateDate();
Date contentUpdate = State.getInstance(content).as(Content.ObjectModification.class).getUpdateDate();
if (wipCreate != null && wipUpdate != null && contentUpdate != null) {
long contentTime = contentUpdate.getTime();
if (wipCreate.getTime() < contentTime && contentTime <= wipUpdate.getTime()) {
wip.delete();
return;
}
}
Map<String, Map<String, Object>> differences = wip.getDifferences();
page.getRequest().setAttribute(WIP_DIFFERENCE_IDS_ATTRIBUTE, differences.keySet());
state.setValues(Draft.mergeDifferences(
state.getDatabase().getEnvironment(),
state.getSimpleValues(),
differences));
page.writeStart("div", "class", "message message-warning WorkInProgressRestoredMessage");
{
page.writeStart("div", "class", "WorkInProgressRestoredMessage-actions");
{
page.writeStart("a",
"class", "icon icon-action-remove",
"href", page.cmsUrl("/user/wips",
"action-delete", true,
"wip", wip.getId(),
"returnUrl", page.url("")));
page.writeHtml(page.localize(wip, "action.clearChanges"));
page.writeEnd();
}
page.writeEnd();
page.writeStart("p");
{
page.writeHtml(page.localize(wip, "message.restored"));
}
page.writeEnd();
}
page.writeEnd();
}
/**
* Returns {@code true} if a work in progress object was restored on top of
* the given {@code object} using {@link #restoreWorkInProgress} in the
* context of the given {@code page}.
*
* @param page Can't be {@code null}.
* @param object Can't be {@code null}.
*/
public static boolean isWorkInProgressRestored(ToolPageContext page, Object object) {
@SuppressWarnings("unchecked")
Set<String> differenceIds = (Set<String>) page.getRequest().getAttribute(WIP_DIFFERENCE_IDS_ATTRIBUTE);
if (differenceIds == null) {
return false;
}
State state = State.getInstance(object);
return differenceIds.contains(state.getId().toString())
|| wipCheckObject(differenceIds, state.getSimpleValues());
}
@SuppressWarnings("unchecked")
private static boolean wipCheckObject(Set<String> differenceIds, Object object) {
if (object instanceof List) {
return wipCheckCollection(differenceIds, (List<Object>) object);
} else if (object instanceof Map) {
Map<String, Object> map = (Map<String, Object>) object;
String ref = ObjectUtils.to(String.class, map.get("_ref"));
if (ref != null) {
return differenceIds.contains(ref);
}
String id = ObjectUtils.to(String.class, map.get(State.ID_KEY));
return (id != null && differenceIds.contains(id))
|| wipCheckCollection(differenceIds, map.values());
} else {
return false;
}
}
private static boolean wipCheckCollection(Set<String> differenceIds, Collection<Object> collection) {
return collection.stream().anyMatch(v -> wipCheckObject(differenceIds, v));
}
/**
* @param page Nonnull.
* @param content Nonnull.
* @param placement Nonnull.
*/
public static void writeWidgets(ToolPageContext page, Object content, ContentEditWidgetPlacement placement) throws IOException {
Preconditions.checkNotNull(page);
Preconditions.checkNotNull(content);
Preconditions.checkNotNull(placement);
String legacyPosition = placement.getLegacyPosition();
if (legacyPosition != null) {
page.writeStart("div", "class", "contentWidgets contentWidgets-" + legacyPosition);
writeLegacyWidgets(page, content, legacyPosition);
}
List<ContentEditWidget> widgets = getWidgets(content);
widgets.sort(Comparator
.comparing((Function<ContentEditWidget, Double>) w -> w.getPosition(page, content, placement))
.thenComparing(w -> w.getClass().getName()));
for (ContentEditWidget widget : widgets) {
ContentEditWidgetPlacement widgetPlacement = widget.getPlacementOverride();
if (widgetPlacement == null) {
widgetPlacement = widget.getPlacement(page, content);
}
if (placement.equals(widgetPlacement)
&& widget.shouldDisplay(page, content)) {
if (widget instanceof UpdatingContentEditWidget) {
writeWidgetOrError(page, content, placement, widget);
} else {
ContentEditWidgetFilter.writeFrame(page, content, placement, widget);
}
}
}
if (legacyPosition != null) {
page.writeEnd();
}
}
private static void writeLegacyWidgets(ToolPageContext page, Object content, String position) throws IOException {
List<Widget> widgets = Tool.Static.getWidgets(position).stream().findFirst().orElse(null);
if (ObjectUtils.isBlank(widgets)) {
return;
}
State state = State.getInstance(content);
ObjectType type = state.getType();
for (Widget widget : widgets) {
String internalName = widget.getInternalName();
if (content instanceof ContentEditWidgetDisplay) {
if (!((ContentEditWidgetDisplay) content).shouldDisplayContentEditWidget(internalName)) {
continue;
}
} else if ((type == null
|| !type.as(ToolUi.class).isPublishable())
&& !widget.shouldDisplayInNonPublishable()) {
continue;
}
if (!page.hasPermission(widget.getPermissionId())) {
continue;
}
page.writeElement("input",
"type", "hidden",
"name", state.getId() + "/_widget",
"value", internalName);
String displayHtml;
try {
displayHtml = widget.createDisplayHtml(page, content);
} catch (Exception error) {
displayHtml = createErrorHtml(error);
}
if (!ObjectUtils.isBlank(displayHtml)) {
page.write(displayHtml);
}
}
}
private static List<ContentEditWidget> getWidgets(Object content) {
List<ContentEditWidget> widgets = new ArrayList<>();
CmsTool cms = Query.from(CmsTool.class).first();
if (cms != null) {
List<ContentEditWidget> cmsWidgets = cms.getContentEditWidgets();
if (cmsWidgets != null) {
cmsWidgets.stream()
.filter(Objects::nonNull)
.forEach(widgets::add);
}
}
ClassFinder.findConcreteClasses(ContentEditWidget.class)
.stream()
.filter(c -> widgets.stream().noneMatch(c::isInstance))
.sorted(Comparator.comparing(Class::getName))
.map(c -> TypeDefinition.getInstance(c).newInstance())
.forEach(widgets::add);
if (content instanceof ContentEditWidgetDisplay) {
widgets.removeIf(w -> !((ContentEditWidgetDisplay) content).shouldDisplayContentEditWidget(w.getClass().getName()));
}
return widgets;
}
/**
* @param page Nonnull.
* @param content Nonnull.
* @param placement Nonnull.
* @param widget Nonnull.
*/
public static void writeWidgetOrError(ToolPageContext page, Object content, ContentEditWidgetPlacement placement, ContentEditWidget widget) throws IOException {
Preconditions.checkNotNull(page);
Preconditions.checkNotNull(content);
Preconditions.checkNotNull(placement);
Preconditions.checkNotNull(widget);
String widgetHtml;
try {
ToolPageContext pageCopy = new ToolPageContext(page.getServletContext(), page.getRequest(), page.getResponse());
StringWriter widgetHtmlWriter = new StringWriter();
pageCopy.setDelegate(widgetHtmlWriter);
widget.display(pageCopy, content, placement);
widgetHtml = widgetHtmlWriter.toString();
} catch (Exception error) {
Throwables.propagateIfInstanceOf(error, IOException.class);
widgetHtml = createErrorHtml(error);
}
if (!ObjectUtils.isBlank(widgetHtml)) {
placement.displayBefore(page, content, widget);
page.write(widgetHtml);
placement.displayAfter(page);
}
}
private static String createErrorHtml(Throwable error) throws IOException {
StringWriter errorString = new StringWriter();
HtmlWriter errorHtml = new HtmlWriter(errorString);
errorHtml.putAllStandardDefaults();
errorHtml.writeStart("pre", "class", "message message-error");
errorHtml.writeObject(error);
errorHtml.writeEnd();
return errorString.toString();
}
/**
* @param page Nonnull.
* @param content Nonnull.
*/
@SuppressWarnings("deprecation")
public static void updateUsingWidgets(ToolPageContext page, Object content) throws Exception {
Preconditions.checkNotNull(page);
Preconditions.checkNotNull(content);
State state = State.getInstance(content);
List<String> requestWidgets = page.params(String.class, state.getId() + "/_widget");
if (!requestWidgets.isEmpty()) {
DependencyResolver<Widget> widgets = new DependencyResolver<>();
for (Widget widget : Tool.Static.getPluginsByClass(Widget.class)) {
widgets.addRequired(widget, widget.getUpdateDependencies());
}
for (Widget widget : widgets.resolve()) {
for (String requestWidget : requestWidgets) {
if (widget.getInternalName().equals(requestWidget)) {
widget.update(page, content);
break;
}
}
}
}
Page.Layout layout = (Page.Layout) page.getRequest().getAttribute("layoutHack");
if (layout != null) {
((Page) content).setLayout(layout);
}
List<ContentEditWidget> widgets = getWidgets(content);
DependencyResolver<UpdatingContentEditWidget> updatingWidgets = new DependencyResolver<>();
for (ContentEditWidget widget : widgets) {
if (widget instanceof UpdatingContentEditWidget) {
UpdatingContentEditWidget updatingWidget = (UpdatingContentEditWidget) widget;
Collection<Class<? extends UpdatingContentEditWidget>> dependencies = updatingWidget.getUpdateDependencies();
if (dependencies != null) {
updatingWidgets.addRequired(
updatingWidget,
widgets.stream()
.filter(w -> dependencies.stream().anyMatch(c -> c.isInstance(w)))
.map(w -> (UpdatingContentEditWidget) w)
.collect(Collectors.toList()));
} else {
updatingWidgets.addRequired(updatingWidget);
}
}
}
for (UpdatingContentEditWidget widget : updatingWidgets.resolve()) {
widget.displayOrUpdate(page, content, null);
}
}
}