package divconq.cms.feed;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map.Entry;
import divconq.io.CacheFile;
import divconq.lang.op.FuncResult;
import divconq.lang.op.OperationContext;
import divconq.lang.op.OperationResult;
import divconq.locale.ITranslationAdapter;
import divconq.locale.LocaleDefinition;
import divconq.log.Logger;
import divconq.util.IOUtil;
import divconq.util.StringUtil;
import divconq.web.WebContext;
import divconq.web.md.Processor;
import divconq.xml.XElement;
import divconq.xml.XNode;
import divconq.xml.XmlReader;
public class FeedAdapter {
protected String channel = null;
protected Path path = null;
protected XElement xml = null;
// best not to use CacheFile always, not if from feed save/feed delete
public void init(String channel, Path path) {
if (path == null)
return;
this.path = path;
this.channel = channel;
if (Files.notExists(path))
return;
FuncResult<XElement> res = XmlReader.loadFile(path, false);
if (res.hasErrors())
OperationContext.get().error("Bad feed file - " + this.channel + " | " + path);
this.xml = res.getResult();
}
public void init(String channel, CacheFile file) {
if (file == null)
return;
this.path = file.getFilePath();
this.channel = channel;
this.xml = file.asXml();
}
public void validate() {
if (this.xml == null)
return;
String locale = this.getAttribute("Locale");
if (StringUtil.isEmpty(locale))
OperationContext.get().error("Missing Locale - " + this.channel + " | " + path);
String title = this.getDefaultField("Title");
if (StringUtil.isEmpty(title))
OperationContext.get().error("Missing Title - " + this.channel + " | " + path);
String desc = this.getDefaultField("Description");
if (StringUtil.isEmpty(desc))
OperationContext.get().warn("Missing Description - " + this.channel + " | " + path);
String key = this.getDefaultField("Keywords");
if (StringUtil.isEmpty(key))
OperationContext.get().warn("Missing Keywords - " + this.channel + " | " + path);
//String img = this.getField("Image");
//if (StringUtil.isEmpty(img))
// OperationContext.get().warn("Missing Image - " + this.key + " | " + path);
}
public String getAttribute(String name) {
if ((this.xml == null) || StringUtil.isEmpty(name))
return null;
return this.xml.getAttribute(name);
}
public String getDefaultField(String name) {
if ((this.xml == null) || StringUtil.isEmpty(name))
return null;
// provide the value for the `default` locale of the feed
String deflocale = this.xml.getAttribute("Locale");
for (XElement fel : this.xml.selectAll("Field")) {
if (name.equals(fel.getAttribute("Name"))) {
if (!fel.hasAttribute("Locale"))
return fel.getValue();
if ((deflocale != null) && deflocale.equals(fel.getAttribute("Locale")))
return fel.getValue();
}
}
return null;
}
public MatchResult bestMatch(String tag, String attr, String name) {
return this.bestMatch(OperationContext.get(), tag, attr, name);
}
public MatchResult bestMatch(ITranslationAdapter ctx, String tag, String attr, String match) {
if ((this.xml == null) || StringUtil.isEmpty(tag) || StringUtil.isEmpty(attr) || StringUtil.isEmpty(match))
return null;
int highest = Integer.MAX_VALUE;
MatchResult best = new MatchResult();
for (XElement afel : this.xml.selectAll("Alternate")) {
String alocale = afel.getAttribute("Locale");
int arate = ctx.rateLocale(alocale);
if ((arate >= highest) || (arate == -1))
continue;
for (XElement fel : afel.selectAll(tag)) {
if (match.equals(fel.getAttribute(attr))) {
best.el = fel;
best.localename = alocale;
highest = arate;
break;
}
}
}
for (XElement fel : this.xml.selectAll(tag)) {
if (fel.hasAttribute("Locale") && match.equals(fel.getAttribute(attr))) {
String flocale = fel.getAttribute("Locale");
int arate = ctx.rateLocale(flocale);
if ((arate >= highest) || (arate == -1))
continue;
best.el = fel;
best.localename = flocale;
highest = arate;
}
}
String deflocale = this.xml.getAttribute("Locale");
if (StringUtil.isNotEmpty(deflocale)) {
int arate = ctx.rateLocale(deflocale);
if ((arate != -1) && (arate < highest)) {
for (XElement fel : this.xml.selectAll(tag)) {
if (!fel.hasAttribute("Locale") && match.equals(fel.getAttribute(attr))) {
best.el = fel;
best.localename = deflocale;
highest = arate;
break;
}
}
}
}
if (highest == Integer.MAX_VALUE)
return null;
best.locale = ctx.getLocaleDefinition(best.localename);
return best;
}
public class MatchResult {
public XElement el = null;
public LocaleDefinition locale = null;
public String localename = null;
}
public String getFirstField(String... names) {
return this.getFirstField(OperationContext.get(), names);
}
public String getFirstField(ITranslationAdapter ctx, String... names) {
for (String n : names) {
String v = this.getField(n);
if (v != null)
return v;
}
return null;
}
public String getField(String name) {
return this.getField(OperationContext.get(), name);
}
public String getField(ITranslationAdapter ctx, String name) {
if ((this.xml == null) || StringUtil.isEmpty(name))
return null;
MatchResult mr = this.bestMatch(ctx, "Field", "Name", name);
if (mr != null)
return mr.el.getValue();
return null;
}
public String getPart(WebContext wctx, String name) {
return this.getPart(wctx, OperationContext.get(), name);
}
public String getPart(WebContext wctx, ITranslationAdapter ctx, String name) {
if ((this.xml == null) || StringUtil.isEmpty(name))
return null;
MatchResult mr = this.bestMatch(ctx, "PagePart", "For", name);
if (mr == null)
return null;
return this.getPartValue(wctx, ctx, mr);
}
public void buildHtmlPage(WebContext wctx, XElement frag) {
this.buildHtmlPage(wctx, OperationContext.get(), frag);
}
public void buildHtmlPage(WebContext wctx, ITranslationAdapter ctx, XElement frag) {
OperationResult or = new OperationResult();
String title = this.getField(ctx, "Title");
if (title != null)
frag.setAttribute("Title", title);
String desc = this.getField(ctx, "Description");
if (desc != null)
frag.add(new XElement("Description").withText(desc));
String keywords = this.getField(ctx, "Keywords");
if (keywords != null)
frag.add(new XElement("Keywords").withText(keywords));
for (XElement pdef : frag.selectAll("PagePartDef")) {
String bid = pdef.getAttribute("BuildId", pdef.getAttribute("For"));
if (StringUtil.isEmpty(bid)) {
or.error("Unable to build page element: " + pdef);
continue;
}
XElement bbparent = frag.findParentOfId(bid);
XElement bparent = bbparent.findId(bid);
if (bparent == null) {
or.error("Missing parent to build page element: " + pdef);
continue;
}
XElement content = this.buildHtml(wctx, ctx, pdef.getAttribute("For"), pdef.getAttribute("BuildClass"), frag);
if (content == null)
continue;
String bop = pdef.getAttribute("BuildOp", "Append");
if ("Append".equals(bop)) {
bparent.add(-1, content);
}
else if ("Prepend".equals(bop)) {
bparent.add(0, content);
}
else if ("Before".equals(bop)) {
int ccnt = bbparent.getChildCount();
int cpos = 0;
for (int i = 0; i < ccnt; i++)
if (bbparent.getChild(i) == bparent) {
cpos = i;
break;
}
bbparent.add(cpos, content);
}
else if ("After".equals(bop)) {
int ccnt = bbparent.getChildCount();
int cpos = 0;
for (int i = 0; i < ccnt; i++)
if (bbparent.getChild(i) == bparent) {
cpos = i;
break;
}
bbparent.add(cpos + 1, content);
}
}
}
public XElement buildHtml(WebContext wctx, String id, String clss, XElement altsrc) {
return this.buildHtml(wctx, OperationContext.get(), id, clss, altsrc);
}
public XElement buildHtml(WebContext wctx, ITranslationAdapter ctx, String id, String clss, XElement altsrc) {
if ((this.xml == null) || StringUtil.isEmpty(id))
return null;
MatchResult mr = this.bestMatch(ctx, "PagePart", "For", id);
if (mr == null) {
if (altsrc != null) {
for (XElement fel : altsrc.selectAll("PagePart")) {
if (id.equals(fel.getAttribute("For"))) {
mr = new MatchResult();
mr.el = fel;
mr.localename = this.xml.getAttribute("Locale", "en");
mr.locale = ctx.getLocaleDefinition(mr.localename);
}
}
}
}
if (mr == null)
return null;
return this.buildHtmlPart(wctx, ctx, mr, id, clss);
}
public XElement buildHtmlPart(WebContext wctx, ITranslationAdapter ctx, MatchResult mr, String id, String clss) {
String lang = mr.locale.getLanguage();
String fmt = mr.el.getAttribute("Format", "md");
XElement pel = new XElement("div");
pel.setAttribute("lang", lang);
if (id != null)
pel.setAttribute("id", id);
if (clss != null)
pel.setAttribute("class", clss);
// copy all attributes
for (Entry<String, String> attr : mr.el.getAttributes().entrySet())
pel.setAttribute(attr.getKey(), attr.getValue());
if ("image".equals(fmt)) {
pel.setName("img");
pel.setAttribute("src", "/galleries" + this.getPartValue(wctx, ctx, mr));
}
else if ("html".equals(fmt)) {
pel.setAttribute("data-dcui-mode", "enhance");
XElement html = this.getPartXml(wctx, ctx, mr);
// copy all children
for (XNode n : html.getChildren())
pel.add(n);
}
else {
pel.setAttribute("data-dcui-mode", "enhance");
try {
// TODO support safe mode?
XElement html = Processor.parse(wctx.getMarkdownContext(), this.getPartValue(wctx, ctx, mr));
// copy all children
for (XNode n : html.getChildren())
pel.add(n);
}
catch (Exception x) {
Logger.error("Error adding copy box" + x);
}
}
return pel;
}
public String getPartValue(WebContext wctx, ITranslationAdapter ctx, MatchResult mr) {
if ((ctx == null) || (mr == null))
return null;
String ex = mr.el.getAttribute("External", "False");
String locale = mr.locale.getName();
if (StringUtil.isNotEmpty(ex) && "true".equals(ex.toLowerCase())) {
int pos = this.channel.indexOf('.');
String spath = (pos != -1) ? this.channel.substring(0, pos) : this.channel;
spath = spath + "." + mr.el.getAttribute("For") + "." + locale + "." + mr.el.getAttribute("Format");
// TODO connect to file caching system - but make sure the import from FeedIndexer will still work correctly
//Path fpath = OperationContext.get().getDomain().findSectionFile(this.isPreview(), "feed", spath);
Path fpath = null;
if (wctx.isPreview()) {
fpath = OperationContext.get().getDomain().resolvePath("/feed-preview" + spath);
if (Files.notExists(fpath))
fpath = OperationContext.get().getDomain().resolvePath("/feed" + spath);
}
else {
fpath = OperationContext.get().getDomain().resolvePath("/feed" + spath);
}
if (Files.exists(fpath)) {
FuncResult<CharSequence> mres = IOUtil.readEntireFile(fpath);
if (mres.isNotEmptyResult())
return mres.getResult().toString();
}
}
return mr.el.getValue();
}
// element returned is ignored - children and attributes are copied into a div
public XElement getPartXml(WebContext wctx, ITranslationAdapter ctx, MatchResult mr) {
if ((ctx == null) || (mr == null))
return null;
String ex = mr.el.getAttribute("External", "False");
String locale = mr.locale.getName();
if (StringUtil.isNotEmpty(ex) && "true".equals(ex.toLowerCase())) {
int pos = this.channel.indexOf('.');
String spath = (pos != -1) ? this.channel.substring(0, pos) : this.channel;
spath = spath + "." + mr.el.getAttribute("For") + "." + locale + "." + mr.el.getAttribute("Format");
// TODO connect to file caching system - but make sure the import from FeedIndexer will still work correctly
//Path fpath = OperationContext.get().getDomain().findSectionFile(this.isPreview(), "feed", spath);
Path fpath = null;
if (wctx.isPreview()) {
fpath = OperationContext.get().getDomain().resolvePath("/feed-preview" + spath);
if (Files.notExists(fpath))
fpath = OperationContext.get().getDomain().resolvePath("/feed" + spath);
}
else {
fpath = OperationContext.get().getDomain().resolvePath("/feed" + spath);
}
if (Files.exists(fpath)) {
FuncResult<CharSequence> mres = IOUtil.readEntireFile(fpath);
if (mres.isNotEmptyResult()) {
FuncResult<XElement> xres = XmlReader.parse("<div>" + mres.getResult() + "</div>", false);
if (xres.isNotEmptyResult())
return xres.getResult();
}
}
}
return mr.el;
}
public XElement getXml() {
return this.xml;
}
public String getPartValue(String locale, XElement part, boolean isPreview) {
if (part == null)
return null;
String ex = part.getAttribute("External", "False");
if (part.hasAttribute("Locale"))
locale = part.getAttribute("Locale"); // use the override locale if present
if (StringUtil.isNotEmpty(ex) && "true".equals(ex.toLowerCase())) {
int pos = this.channel.indexOf('.');
String spath = (pos != -1) ? this.channel.substring(0, pos) : this.channel;
spath = spath + "." + part.getAttribute("For") + "." + locale + "." + part.getAttribute("Format");
// TODO connect to file caching system - but make sure the import from FeedIndexer will still work correctly
//Path fpath = OperationContext.get().getDomain().findSectionFile(this.isPreview(), "feed", spath);
Path fpath = null;
if (isPreview) {
fpath = OperationContext.get().getDomain().resolvePath("/feed-preview" + spath);
if (Files.notExists(fpath))
fpath = OperationContext.get().getDomain().resolvePath("/feed" + spath);
}
else {
fpath = OperationContext.get().getDomain().resolvePath("/feed" + spath);
}
if (Files.exists(fpath)) {
FuncResult<CharSequence> mres = IOUtil.readEntireFile(fpath);
if (mres.isNotEmptyResult())
return mres.getResult().toString();
}
}
return part.getValue();
}
}