package divconq.cms.feed; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.List; import java.util.function.BiConsumer; import divconq.db.DataRequest; import divconq.db.ObjectResult; import divconq.db.ReplicatedDataRequest; import divconq.hub.DomainInfo; import divconq.hub.Hub; import divconq.lang.op.FuncCallback; import divconq.lang.op.FuncResult; import divconq.lang.op.OperationCallback; import divconq.lang.op.OperationContext; import divconq.lang.op.OperationResult; import divconq.struct.CompositeStruct; import divconq.struct.ListStruct; import divconq.struct.RecordStruct; import divconq.struct.Struct; import divconq.util.IOUtil; import divconq.util.StringUtil; import divconq.xml.XElement; import divconq.xml.XmlReader; public class FeedInfo { public static FeedInfo recordToInfo(RecordStruct rec) { String channel = rec.getFieldAsString("Channel"); String path = rec.getFieldAsString("Path"); String site = rec.getFieldAsString("Site"); // default to root if (StringUtil.isEmpty(site)) site = "root"; return FeedInfo.buildInfo(site, channel, path); } public static FeedInfo buildInfo(String site, String channel, String path) { if (StringUtil.isEmpty(channel) || StringUtil.isEmpty(path)) return null; if (StringUtil.isEmpty(site)) site = "root"; XElement channelDef = FeedIndexer.findChannel(site, channel); // if channelDef is null then it is not allowed for this site or does not exist if (channelDef == null) return null; FeedInfo fi = new FeedInfo(); fi.site = site; fi.channel = channel; fi.outerpath = path; fi.channelDef = channelDef; fi.init(); return fi; } protected String site = null; protected String channel = null; protected String outerpath = null; protected String innerpath = null; protected XElement channelDef = null; protected XElement draftDcfContent = null; protected XElement pubDcfContent = null; protected Path pubpath = null; protected Path prepath = null; // for this work correctly you need to set site, channel and path first public void init() { if (this.channelDef == null) return; this.innerpath = this.channelDef.getAttribute("InnerPath", "") + this.outerpath; // InnerPath or empty string String prepath = "root".equals(site) ? "/feed-preview" + this.innerpath + ".dcf.xml" : "/sites/" + site + "/feed-preview" + this.innerpath + ".dcf.xml"; String ppath = "root".equals(site) ? "/feed" + this.innerpath + ".dcf.xml" : "/sites/" + site + "/feed" + this.innerpath + ".dcf.xml"; DomainInfo domain = OperationContext.get().getUserContext().getDomain(); this.prepath = domain.resolvePath(prepath).toAbsolutePath().normalize(); this.pubpath = domain.resolvePath(ppath).toAbsolutePath().normalize(); } public List<String> collectExternalFileNames(boolean draft) { ArrayList<String> list = new ArrayList<String>(); // load the feed file XElement dcf = draft ? this.getDraftDcfContent() : this.getPubDcfContent(); if (dcf == null) return list; // collect all the external part names BiConsumer<XElement, String> collectfunc = new BiConsumer<XElement, String>() { @Override public void accept(XElement part, String locale) { // don't move if not external String ext = part.getAttribute("External", "false").toLowerCase(); if (!"true".equals(ext)) return; // use the override locale if present if (part.hasAttribute("Locale")) locale = part.getAttribute("Locale"); String sname = (draft ? FeedInfo.this.prepath : FeedInfo.this.pubpath).getFileName().toString(); int pos = sname.indexOf('.'); sname = sname.substring(0, pos) + "." + part.getAttribute("For") + "." + locale + "." + part.getAttribute("Format"); list.add(sname); } }; // TODO really this should be the Site default locale String deflocale = dcf.getAttribute("Locale", OperationContext.get().getDomain().getDefaultLocale()); // check for external parts and move them for (XElement fel : dcf.selectAll("PagePart")) collectfunc.accept(fel, deflocale); for (XElement afel : dcf.selectAll("Alternate")) { String locale = afel.getAttribute("Locale"); for (XElement fel : afel.selectAll("PagePart")) collectfunc.accept(fel, locale); } return list; } public String getSite() { return this.site; } public String getChannel() { return this.channel; } public String getInnerPath() { return this.innerpath; } public String getOuterPath() { return this.outerpath; } public XElement getChannelDef() { return this.channelDef; } public XElement getDraftDcfContent() { if (this.draftDcfContent == null) { if (Files.exists(this.prepath)) { FuncResult<XElement> res = XmlReader.loadFile(this.prepath, false); this.draftDcfContent = res.getResult(); } } return this.draftDcfContent; } public XElement getPubDcfContent() { if (this.pubDcfContent == null) { if (Files.exists(this.pubpath)) { FuncResult<XElement> res = XmlReader.loadFile(this.pubpath, false); this.pubDcfContent = res.getResult(); } } return this.pubDcfContent; } public Path getPubpath() { return this.pubpath; } public Path getPrepath() { return this.prepath; } public FeedAdapter getPreAdapter() { OperationResult op = new OperationResult(); FeedAdapter adapt = new FeedAdapter(); adapt.init(this.channel, this.prepath); // load direct, not cache - cache may not have updated yet adapt.validate(); // if an error occurred during the init or validate, don't use the feed if (op.hasErrors()) return null; return adapt; } public FeedAdapter getPubAdapter() { OperationResult op = new OperationResult(); FeedAdapter adapt = new FeedAdapter(); adapt.init(this.channel, this.pubpath); // load direct, not cache - cache may not have updated yet adapt.validate(); // if an error occurred during the init or validate, don't use the feed if (op.hasErrors()) return null; return adapt; } // this is not required, you may go right to saveDraftFile // dcui = only if a Page public void initDraftFile(String locale, String title, String dcui, FuncCallback<CompositeStruct> op) { if (StringUtil.isEmpty(locale)) locale = OperationContext.get().getDomain().getDefaultLocale(); // TODO really want from the Site if ("Pages".equals(this.getChannel()) && StringUtil.isNotEmpty(dcui)) { DomainInfo domain = OperationContext.get().getDomain(); // don't go to www-preview at first, www-preview would only be used by a developer showing an altered page // for first time save, it makes sense to have the dcui file in www Path uisrcpath = domain.resolvePath("/www" + this.getOuterPath() + ".dcui.xml"); // TODO per site try { Files.createDirectories(uisrcpath.getParent()); IOUtil.saveEntireFile(uisrcpath, dcui); } catch (Exception x) { op.error("Unable to add dcui file: " + x); op.complete(); return; } } try { Files.createDirectories(this.getPrepath().getParent()); } catch (Exception x) { op.error("Unable to create draft folder: " + x); op.complete(); return; } XElement root = new XElement("dcf") .withAttribute("Locale", locale) .with(new XElement("Field") .withAttribute("Value", title) .withAttribute("Name", "Title") ); // TODO clear the CacheFile index for this path so that we get up to date entries when importing IOUtil.saveEntireFile(this.getPrepath(), root.toString(true)); this.updateDb(op); } // dcf = content for the dcf file // updates = list of records (Name, Content) to write out // deletes = list of filenames to remove public void saveFile(boolean draft, XElement dcf, ListStruct updates, ListStruct deletes, FuncCallback<CompositeStruct> op) { Path savepath = draft ? this.getPrepath() : this.getPubpath(); try { Files.createDirectories(savepath.getParent()); } catch (Exception x) { op.error("Unable to create draft folder: " + x); op.complete(); return; } String locale = dcf.getAttribute("Locale"); if (StringUtil.isEmpty(locale)) dcf.setAttribute("Locale", OperationContext.get().getDomain().getDefaultLocale()); // TODO really want from the Site try { if (deletes != null) for (Struct df : deletes.getItems()) // TODO clear the CacheFile index for this path so that we get up to date entries when importing Files.deleteIfExists(savepath.resolveSibling(df.toString())); if (updates != null) for (Struct uf : updates.getItems()) { RecordStruct urec = (RecordStruct) uf; // TODO clear the CacheFile index for this path so that we get up to date entries when importing IOUtil.saveEntireFile(savepath.resolveSibling(urec.getFieldAsString("Name")), urec.getFieldAsString("Content")); } if (dcf != null) // TODO clear the CacheFile index for this path so that we get up to date entries when importing IOUtil.saveEntireFile(savepath, dcf.toString(true)); // cleanup any draft files, we skipped over them if (!draft) { List<String> filelist = this.collectExternalFileNames(true); // move all the external files for (String sname : filelist) { try { Files.deleteIfExists(this.getPrepath().resolveSibling(sname)); // TODO clear the CacheFile index for this path so that we get up to date entries when importing } catch (Exception x) { } } // finally move the feed file itself try { Files.deleteIfExists(this.getPrepath()); // TODO clear the CacheFile index for this path so that we get up to date entries when importing } catch (Exception x) { op.complete(); } } this.updateDb(op); } catch (Exception x) { op.error("Unable to update feed: " + x); op.complete(); } } public void publicizeFile(FuncCallback<CompositeStruct> op) { // if no preview available then nothing we can do here if (Files.notExists(this.getPrepath())) { op.complete(); return; } try { Files.createDirectories(this.getPubpath().getParent()); } catch (Exception x) { op.error("Unable to create publish folder: " + x); op.complete(); return; } List<String> filelist = this.collectExternalFileNames(true); // move all the external files for (String sname : filelist) { Path ypath = this.getPrepath().resolveSibling(sname); // don't bother if there is no preview file if (Files.notExists(ypath)) continue; try { Files.move(ypath, this.getPubpath().resolveSibling(sname), StandardCopyOption.REPLACE_EXISTING); // TODO clear the CacheFile index for this path so that we get up to date entries when importing } catch (Exception x) { op.error("Unable to move preview file: " + ypath + " : " + x); } } // finally move the feed file itself try { Files.move(this.getPrepath(), this.getPubpath(), StandardCopyOption.REPLACE_EXISTING); // TODO clear the CacheFile index for this path so that we get up to date entries when importing this.updateDb(op); } catch (Exception x) { op.error("Unable to move preview file: " + this.getPrepath() + " : " + x); op.complete(); } } public void deleteFile(DeleteMode mode, OperationCallback op) { for (int i = 0; i < 2; i++) { boolean draft = (i == 0); if (draft && (mode == DeleteMode.Published)) continue; if (!draft && (mode == DeleteMode.Draft)) continue; Path fpath = draft ? this.getPrepath() : this.getPubpath(); // if no dcf file available then nothing we can do if (Files.notExists(fpath)) continue; List<String> filelist = this.collectExternalFileNames(draft); // move all the external files for (String sname : filelist) { Path ypath = fpath.resolveSibling(sname); try { Files.deleteIfExists(ypath); } catch (Exception x) { op.error("Unable to delete feed external file: " + ypath + " : " + x); } } // finally move the feed file itself try { Files.deleteIfExists(fpath); } catch (Exception x) { op.error("Unable to delete feed file: " + fpath + " : " + x); } String channel = this.getChannel(); String path = this.getOuterPath(); String alias = OperationContext.get().getDomain().getAlias(); // load Page definitions... if ("Pages".equals(channel) || "Block".equals(channel)) { Path srcpath = draft ? Hub.instance.getPublicFileStore().resolvePath("dcw/" + alias + "/www-preview/" + path + ".dcui.xml") : Hub.instance.getPublicFileStore().resolvePath("dcw/" + alias + "/www/" + path + ".dcui.xml"); try { Files.deleteIfExists(srcpath); } catch (Exception x) { } } } this.deleteDb(op); } public void deleteDb(OperationCallback cb) { Hub.instance.getDatabase().submit( new ReplicatedDataRequest("dcmFeedDelete") .withParams(new RecordStruct() // TODO .withField("Site", this.site) .withField("Channel", this.channel) .withField("Path", this.innerpath) ), new ObjectResult() { @Override public void process(CompositeStruct result3b) { cb.complete(); } }); } public void updateDb(OperationCallback cb) { // TODO add sub site indexing, and a conversion to rebuild the indexes if (!this.site.equals("root")) { cb.complete(); return; } // work through the adapters FeedAdapter pubfeed = this.getPubAdapter(); FeedAdapter prefeed = this.getPreAdapter(); if ((pubfeed == null) && (prefeed == null)) { cb.complete(); return; } XElement pubxml = (pubfeed != null) ? pubfeed.getXml() : null; XElement prexml = (prefeed != null) ? prefeed.getXml() : null; // if no file is present then delete record for feed if ((pubxml == null) && (prexml == null)) { this.deleteDb(cb); return; } // if at least one xml file then update/add a record for the feed RecordStruct feed = new RecordStruct() // TODO .withField("Site", this.site) .withField("Channel", this.channel) .withField("Path", this.outerpath) .withField("Editable", true); // the "edit" authorization, not the "view" auth String authtags = (pubfeed != null) ? pubfeed.getAttribute("AuthTags") : prefeed.getAttribute("AuthTags"); if (StringUtil.isEmpty(authtags)) feed.withField("AuthorizationTags", new ListStruct()); else feed.withField("AuthorizationTags", new ListStruct((Object[]) authtags.split(","))); if (pubxml != null) { ListStruct ctags = new ListStruct(); for (XElement tag : pubxml.selectAll("Tag")) { String alias = tag.getAttribute("Alias"); if (StringUtil.isNotEmpty(alias)) ctags.addItem(alias); } feed.withField("ContentTags", ctags); } else if (prexml != null) { ListStruct ctags = new ListStruct(); for (XElement tag : prexml.selectAll("Tag")) { String alias = tag.getAttribute("Alias"); if (StringUtil.isNotEmpty(alias)) ctags.addItem(alias); } feed.withField("ContentTags", ctags); } // we should always have info in the Preview fields - use the published if no draft if (prexml == null) prexml = pubxml; if (pubxml != null) { // public fields String primelocale = pubxml.getAttribute("Locale"); ListStruct pubfields = new ListStruct(); feed.withField("Fields", pubfields); for (XElement fld : pubxml.selectAll("Field")) pubfields.addItem(new RecordStruct() .withField("Name", fld.getAttribute("Name")) .withField("Locale", fld.getAttribute("Locale", primelocale)) // prime locale can be override for field, though it means little besides adding to search info .withField("Value", fld.getValue()) ); for (XElement afel : pubxml.selectAll("Alternate")) { String alocale = afel.getAttribute("Locale"); for (XElement fld : afel.selectAll("Field")) pubfields.addItem(new RecordStruct() .withField("Name", fld.getAttribute("Name")) .withField("Locale", alocale) .withField("Value", fld.getValue()) ); } ListStruct pubparts = new ListStruct(); feed.withField("PartContent", pubparts); for (XElement fld : pubxml.selectAll("PagePart")) pubparts.addItem(new RecordStruct() .withField("Name", fld.getAttribute("For")) .withField("Format", fld.getAttribute("Format")) .withField("Locale", fld.getAttribute("Locale", primelocale)) // prime locale can be override for specific part, this is not an alternate, just the default locale for that part .withField("Value", pubfeed.getPartValue(primelocale, fld, false)) ); for (XElement afel : pubxml.selectAll("Alternate")) { String alocale = afel.getAttribute("Locale"); for (XElement fld : afel.selectAll("PagePart")) pubparts.addItem(new RecordStruct() .withField("Name", fld.getAttribute("For")) .withField("Format", fld.getAttribute("Format")) .withField("Locale", alocale) .withField("Value", pubfeed.getPartValue(alocale, fld, false)) ); } } if (prexml != null) { // preview fields String primelocale = prexml.getAttribute("Locale"); ListStruct prefields = new ListStruct(); feed.withField("PreviewFields", prefields); for (XElement fld : prexml.selectAll("Field")) prefields.addItem(new RecordStruct() .withField("Name", fld.getAttribute("Name")) .withField("Locale", fld.getAttribute("Locale", primelocale)) // prime locale can be override for field, though it means little besides adding to search info .withField("Value", fld.getValue()) ); for (XElement afel : prexml.selectAll("Alternate")) { String alocale = afel.getAttribute("Locale"); for (XElement fld : afel.selectAll("Field")) prefields.addItem(new RecordStruct() .withField("Name", fld.getAttribute("Name")) .withField("Locale", alocale) .withField("Value", fld.getValue()) ); } ListStruct preparts = new ListStruct(); feed.withField("PreviewPartContent", preparts); for (XElement fld : prexml.selectAll("PagePart")) preparts.addItem(new RecordStruct() .withField("Name", fld.getAttribute("For")) .withField("Format", fld.getAttribute("Format")) .withField("Locale", fld.getAttribute("Locale", primelocale)) // prime locale can be override for specific part, this is not an alternate, just the default locale for that part .withField("Value", prefeed.getPartValue(primelocale, fld, true)) ); for (XElement afel : prexml.selectAll("Alternate")) { String alocale = afel.getAttribute("Locale"); for (XElement fld : afel.selectAll("PagePart")) preparts.addItem(new RecordStruct() .withField("Name", fld.getAttribute("For")) .withField("Format", fld.getAttribute("Format")) .withField("Locale", alocale) .withField("Value", prefeed.getPartValue(alocale, fld, true)) ); } } // don't bother checking if it worked in our response to service DataRequest req3b = new ReplicatedDataRequest("dcmFeedUpdate") .withParams(feed); Hub.instance.getDatabase().submit(req3b, new ObjectResult() { @Override public void process(CompositeStruct result3b) { cb.complete(); } }); } }