package com.psddev.cms.db;
import com.psddev.cms.rte.ReferenceRichTextElement;
import com.psddev.cms.tool.ToolPageContext;
import com.psddev.dari.db.ObjectField;
import com.psddev.dari.db.ObjectType;
import com.psddev.dari.db.Record;
import com.psddev.dari.util.CodeUtils;
import com.psddev.dari.util.Lazy;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import com.google.common.base.Preconditions;
import org.jsoup.nodes.Attribute;
import org.jsoup.nodes.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class RichTextElement extends Record {
private static final Logger LOGGER = LoggerFactory.getLogger(RichTextElement.class);
private static final Lazy<Map<String, ObjectType>> CONCRETE_TAG_TYPES = new Lazy<Map<String, ObjectType>>() {
@Override
protected Map<String, ObjectType> create() throws Exception {
Map<String, ObjectType> tagTypes = new LinkedHashMap<>();
ObjectType.getInstance(RichTextElement.class).findConcreteTypes().forEach(type -> {
String tagName = type.as(ToolUi.class).getRichTextElementTagName();
if (tagName != null && type.getObjectClass() != null) {
ObjectType existingType = tagTypes.putIfAbsent(tagName, type);
if (existingType != null) {
LOGGER.warn("Ignoring [{}] because its tag name, [{}], conflicts with [{}]",
new Object[] {
type.getInternalName(),
tagName,
existingType.getInternalName()
});
}
}
});
tagTypes.put(ReferenceRichTextElement.TAG_NAME, ObjectType.getInstance(ReferenceRichTextElement.class));
return Collections.unmodifiableMap(tagTypes);
}
};
static {
CodeUtils.addRedefineClassesListener(classes -> CONCRETE_TAG_TYPES.reset());
}
/**
* Returns all concrete types that extend {@link RichTextElement}, keyed
* by their {@linkplain Tag#value() tag names}.
*
* @return Nonnull. Immutable.
*/
public static Map<String, ObjectType> getConcreteTagTypes() {
return CONCRETE_TAG_TYPES.get();
}
/**
* Returns a newly created {@link RichTextElement} from the given
* {@link Element} or null if there is no concrete rich text element
* type with a {@link Tag#value() tag name} that is equal to the element's
* {@linkplain Element#tagName() tag name}.
*
* @param element Nonnull.
* @return The rich text element.
*/
public static RichTextElement fromElement(Element element) {
Preconditions.checkNotNull(element);
ObjectType tagType = getConcreteTagTypes().get(element.tagName());
if (tagType == null) {
return null;
}
RichTextElement rte = (RichTextElement) tagType.createObject(null);
rte.fromAttributes(StreamSupport
.stream(element.attributes().spliterator(), false)
.collect(Collectors.toMap(Attribute::getKey, Attribute::getValue)));
rte.fromBody(element.html());
return rte;
}
public abstract void fromAttributes(Map<String, String> attributes);
public void fromBody(String body) {
}
public abstract Map<String, String> toAttributes();
public String toBody() {
return null;
}
public boolean shouldCloseOnSave() {
return true;
}
public void writePreviewHtml(ToolPageContext page) throws IOException {
throw new UnsupportedOperationException();
}
@Documented
@ObjectType.AnnotationProcessorClass(TagProcessor.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Tag {
String value();
String initialBody() default "";
boolean block() default false;
boolean preview() default false;
boolean readOnly() default false;
boolean root() default false;
Class<?>[] children() default { };
String menu() default "";
String tooltip() default "";
String[] keymaps() default { };
double position() default 0d;
}
private static class TagProcessor implements ObjectType.AnnotationProcessor<Tag> {
@Override
public void process(ObjectType type, Tag annotation) {
type.as(ToolUi.class).setRichTextElementTagName(annotation.value());
}
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Exclusive {
}
@Documented
@ObjectField.AnnotationProcessorClass(TagsProcessor.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Tags {
Class<?>[] value();
}
private static class TagsProcessor implements ObjectField.AnnotationProcessor<Tags> {
@Override
public void process(ObjectType type, ObjectField field, Tags annotation) {
field.as(ToolUi.class).setRichTextElementClassNames(
Stream.of(annotation.value())
.map(Class::getName)
.collect(Collectors.toCollection(LinkedHashSet::new)));
}
}
}