/*
* Copyright 2015
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package org.openntf.domino.design.impl;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Logger;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import org.openntf.domino.Database;
import org.openntf.domino.Document;
import org.openntf.domino.DxlExporter;
import org.openntf.domino.DxlImporter;
import org.openntf.domino.Session;
import org.openntf.domino.design.DesignBase;
import org.openntf.domino.design.DesignBaseNamed;
import org.openntf.domino.utils.DominoUtils;
import org.openntf.domino.utils.xml.XMLDocument;
import org.openntf.domino.utils.xml.XMLNode;
import org.xml.sax.SAXException;
import com.ibm.commons.util.StringUtil;
/**
* This is the Root class of all DesignNotes
*
* @author jgallagher, Roland Praml
*
*/
@SuppressWarnings("serial")
public abstract class AbstractDesignBase implements DesignBase {
@SuppressWarnings("unused")
private static final Logger log_ = Logger.getLogger(AbstractDesignBase.class.getName());
private static Transformer ODP_META_TRANSFORMER = createTransformer("dxl_metafilter.xslt");
private static final char DESIGN_FLAG_PRESERVE = 'P';
private static final char DESIGN_FLAG_PROPAGATE_NOCHANGE = 'r';
private static final char DESIGN_FLAG_NEEDSREFRESH = '$';
private static final char DESIGN_FLAG_HIDE_FROM_WEB = 'w';
private static final char DESIGN_FLAG_HIDE_FROM_NOTES = 'n';
protected static final String FLAGS_ITEM = "$Flags";
protected static final String FLAGS_EXT_ITEM = "$FlagsExt";
protected static final String TITLE_ITEM = "$TITLE";
private transient Database database_;
private transient Document document_;
private String universalId_;
private XMLDocument dxl_;
private DxlFormat dxlFormat_ = DxlFormat.NONE;
private transient ODPMapping odpMapping_;
private Date lastModified_;
/**
* Create a new DesignBase based on the given database. You may add content to this DesignBase and save it afterwards.
*
* @param database
* the Database
*/
public AbstractDesignBase(final Database database) {
database_ = database;
loadDxl(getClass().getResourceAsStream(getClass().getSimpleName() + ".xml"));
}
/**
* Create a new DesginBase based on the given document. This Method will be invoked by {@link DesignFactory#fromDocument(Document)}
*
* @param document
*/
protected AbstractDesignBase(final Document document) {
setDocument(document);
}
protected DxlFormat getDxlFormat(final boolean detect) {
if (detect)
getDxl();
return dxlFormat_;
}
protected abstract boolean enforceRawFormat();
// /**
// * The preferd ODA Format (to fix bugs)
// * @return
// */
// protected abstract DxlFormat preferedOdaFormat();
// /**
// * Indicates, wether this Note should be exported in Raw-Format.
// */
// protected boolean enforceRawFormat() {
// return true;
// }
/*
* (non-Javadoc)
*
* @see org.openntf.domino.design.DesignBase#isHideFromNotes()
*/
@Override
public final boolean isHideFromNotes() {
switch (getDxlFormat(false)) {
case DXL:
return getDocumentElement().getAttribute("hide").contains("notes");
default:
return hasFlag(DESIGN_FLAG_HIDE_FROM_NOTES);
}
}
/*
* (non-Javadoc)
*
* @see org.openntf.domino.design.DesignBase#isHideFromWeb()
*/
@Override
public final boolean isHideFromWeb() {
switch (getDxlFormat(false)) {
case DXL:
return getDocumentElement().getAttribute("hide").contains("web");
default:
return hasFlag(DESIGN_FLAG_HIDE_FROM_WEB);
}
}
/*
* (non-Javadoc)
*
* @see org.openntf.domino.design.DesignBase#isNeedsRefresh()
*/
@Override
public final boolean isNeedsRefresh() {
switch (getDxlFormat(false)) {
case DXL:
return getDocumentElement().getAttribute("refresh").equals("true");
default:
return hasFlag(DESIGN_FLAG_NEEDSREFRESH);
}
}
/*
* (non-Javadoc)
*
* @see org.openntf.domino.design.DesignBase#isPreventChanges()
*/
@Override
public final boolean isPreventChanges() {
switch (getDxlFormat(false)) {
case DXL:
return getDocumentElement().getAttribute("noreplace").equals("true");
default:
return hasFlag(DESIGN_FLAG_PRESERVE);
}
}
/*
* (non-Javadoc)
*
* @see org.openntf.domino.design.DesignBase#isPropagatePreventChanges()
*/
@Override
public final boolean isPropagatePreventChanges() {
switch (getDxlFormat(false)) {
case DXL:
return getDocumentElement().getAttribute("propagatenoreplace").equals("true");
default:
return hasFlag(DESIGN_FLAG_PROPAGATE_NOCHANGE);
}
}
/*
* Helper for Non-raw Notes
*/
protected void setHide(final String platform, final boolean hide) {
if (getDxlFormat(false) != DxlFormat.DXL)
throw new IllegalStateException("Not in DXL Format");
String platforms = getDxl().getFirstChild().getAttribute("hide");
if (hide) {
if (platforms.contains(platform))
return;
platforms += " " + platform;
} else {
if (!platforms.contains(platform))
return;
platforms = platforms.replace(platform, "");
}
getDocumentElement().setAttribute("hide", platforms.trim());
}
/*
* (non-Javadoc)
*
* @see org.openntf.domino.design.DesignBase#setHideFromNotes(boolean)
*/
@Override
public final void setHideFromNotes(final boolean hideFromNotes) {
switch (getDxlFormat(true)) {
case DXL:
setHide("notes", hideFromNotes);
default:
setFlag(DESIGN_FLAG_HIDE_FROM_NOTES, hideFromNotes);
}
}
/*
* (non-Javadoc)
*
* @see org.openntf.domino.design.DesignBase#setHideFromWeb(boolean)
*/
@Override
public void setHideFromWeb(final boolean hideFromWeb) {
switch (getDxlFormat(true)) {
case DXL:
setHide("web", hideFromWeb);
default:
setFlag(DESIGN_FLAG_HIDE_FROM_WEB, hideFromWeb);
}
}
/*
* (non-Javadoc)
*
* @see org.openntf.domino.design.DesignBase#setNeedsRefresh(boolean)
*/
@Override
public void setNeedsRefresh(final boolean needsRefresh) {
switch (getDxlFormat(true)) {
case DXL:
getDocumentElement().setAttribute("refresh", String.valueOf(needsRefresh));
default:
setFlag(DESIGN_FLAG_NEEDSREFRESH, needsRefresh);
}
}
/*
* (non-Javadoc)
*
* @see org.openntf.domino.design.DesignBase#setPreventChanges(boolean)
*/
@Override
public void setPreventChanges(final boolean preventChanges) {
switch (getDxlFormat(true)) {
case DXL:
getDocumentElement().setAttribute("noreplace", String.valueOf(preventChanges));
default:
setFlag(DESIGN_FLAG_PRESERVE, preventChanges);
}
}
/*
* (non-Javadoc)
*
* @see org.openntf.domino.design.DesignBase#setPropagatePreventChanges(boolean)
*/
@Override
public void setPropagatePreventChanges(final boolean propagatePreventChanges) {
switch (getDxlFormat(true)) {
case DXL:
getDocumentElement().setAttribute("propagatenoreplace", String.valueOf(propagatePreventChanges));
default:
setFlag(DESIGN_FLAG_PROPAGATE_NOCHANGE, propagatePreventChanges);
}
}
// -------------- Flags stuff
protected String getFlags() {
if (getDxlFormat(true) != DxlFormat.RAWNOTE)
throw new IllegalStateException("Flags are available only in DxlFormat.RAWNOTE");
return getItemValueString(FLAGS_ITEM);
}
protected void setFlag(final char flag, final boolean enabled) {
String flags = getFlags();
if (enabled) {
if (flags.indexOf(flag) < 0) {
setItemValue(FLAGS_ITEM, flags + flag, FLAG_SUMMARY);
}
} else {
if (flags.indexOf(flag) >= 0) {
// Assume this works for now
setItemValue(FLAGS_ITEM, flags.replace(flag + "", ""), FLAG_SUMMARY);
}
}
}
protected boolean hasFlag(final char flag) {
return getFlags().indexOf(flag) >= 0;
}
// FlagsExt
protected String getFlagsExt() {
getDxlFormat(true); // as far as I know, this item is also
return getItemValueString(FLAGS_EXT_ITEM);
}
protected void setFlagExt(final char flag, final boolean enabled) {
String flags = getFlagsExt();
if (enabled) {
if (flags.indexOf(flag) < 0) {
setItemValue(FLAGS_EXT_ITEM, flags + flag, FLAG_SIGN_SUMMARY);
}
} else {
if (flags.indexOf(flag) >= 0) {
// Assume this works for now
setItemValue(FLAGS_EXT_ITEM, flags.replace(flag + "", ""), FLAG_SIGN_SUMMARY);
}
}
}
protected boolean hasFlagExt(final char flag) {
return getFlagsExt().indexOf(flag) >= 0;
}
// private static final String NO_ENCODING = "<?xml version='1.0'?>";
// private static final String DEFAULT_ENCODING = "<?xml version='1.0'?>";
/**
* Creates a transformer with the given file resource (in this package)
*
* @param resource
* @return
*/
protected static Transformer createTransformer(final String resource) {
return XMLNode.createTransformer(AbstractDesignBase.class.getResourceAsStream(resource));
}
/**
* Returns, if the resourceName must be encoded
*
* @param resName
* the resourceName
* @return true if the resourceName contains invalid characters
*/
protected boolean mustEncode(final String resName) {
for (int i = 0; i < resName.length(); i++) {
char ch = resName.charAt(i);
switch (ch) {
case '/':
case '\\':
case ':':
case '*':
case '?':
case '<':
case '>':
case '|':
case '"':
return true;
}
}
return false;
}
/**
* Encodes the resource name, so that it is ODP-compatible
*
* @param resName
* the resource name
* @return the encoded version (replaces / \ : * > < | " )
*/
protected String encodeResourceName(final String resName) {
if (resName == null)
return null;
if (!mustEncode(resName))
return resName;
StringBuffer sb = new StringBuffer();
for (int i = 0; i < resName.length(); i++) {
char ch = resName.charAt(i);
switch (ch) {
case '_':
case '/':
case '\\':
case ':':
case '*':
case '?':
case '<':
case '>':
case '|':
case '"':
sb.append('_');
sb.append(Integer.toHexString(ch));
break;
default:
sb.append(ch);
}
}
return sb.toString();
}
// /**
// * Returns the folder of this designelemnt in the ODP
// *
// * @return the folder (e.g. Code/Java)
// */
// @Override
// public abstract String getOnDiskFolder();
/**
* Returns the name of this resource
*
* @return the name (e.g. org/openntf/myJavaClass)
*/
protected String getOnDiskName() {
String odpName = getOdpMapping().getFileName();
String extension = "";
String ret;
if (odpName == null) { // no name specified
if (this instanceof DesignBaseNamed) {
ret = encodeResourceName(((DesignBaseNamed) this).getName());
} else {
ret = getUniversalID();
}
} else if (odpName.startsWith("*")) {
ret = ((DesignBaseNamed) this).getName();
} else if (!odpName.startsWith(".")) {
ret = odpName;
} else {
if (this instanceof DesignBaseNamed) {
ret = encodeResourceName(((DesignBaseNamed) this).getName());
} else {
ret = getUniversalID();
}
extension = odpName;
}
if (!ret.endsWith(extension))
ret = ret + extension;
return ret;
}
// /**
// * Returns the extension of the ODP-file (e.g. ".java")
// *
// * @return the extension
// */
// protected String getOnDiskExtension() {
// return "";
// }
protected ODPMapping getOdpMapping() {
if (odpMapping_ == null) {
odpMapping_ = ODPMapping.valueOf(getClass());
}
return odpMapping_;
}
/**
* Returns the full path in an ODP of this resoruce
*
* @return the full path (e.g. Code/Java/org/openntf/myJavaClass.java)
*/
public String getOnDiskPath() {
String path = getOdpMapping().getFolder();
if (path == null)
return null;
if (path.length() > 0) {
path = path + "/" + getOnDiskName();
} else {
path = getOnDiskName();
}
return path;
}
/**
* Returns the transformer that should be used to clean up the dxl-output
*
* @return the transformer
*/
protected Transformer getOdpTransformer() {
return null;
}
/**
* Returns the transformer that should be used to clean up the MetaData-Output
*
* @return the transformer for ".metadata" file
*/
protected Transformer getOdpMetaTransformer() {
return ODP_META_TRANSFORMER;
}
// TODO
@Override
public void writeOnDiskFile(final File odpFile) throws IOException {
getDxl().getXml(getOdpTransformer(), odpFile);
odpFile.setLastModified(getDocLastModified().getTime());
}
// TODO
public final void writeOnDiskMeta(final File odpFile) throws IOException {
getDxl().getXml(getOdpMetaTransformer(), odpFile);
odpFile.setLastModified(getDocLastModified().getTime());
}
//------------------------------ ondisk end --------------------------------
@Override
public final void reattach(final Database database) {
database_ = database;
}
protected final void setDocument(final Document document) {
database_ = document.getAncestorDatabase();
universalId_ = document.getUniversalID(); // we must save the UNID. because NoteID may change on various instances
document_ = document;
lastModified_ = document.getLastModifiedDate();
dxl_ = null;
}
/* (non-Javadoc)
* @see org.openntf.domino.design.DesignBase#getDxlString()
*/
@Override
public final String getDxlString(final Transformer filter) {
try {
return getDxl().getXml(filter);
} catch (IOException e) {
DominoUtils.handleException(e);
return null;
}
}
/* (non-Javadoc)
*
* @see org.openntf.domino.types.DatabaseDescendant#getAncestorDatabase()
*/
@Override
public final Database getAncestorDatabase() {
return database_;
}
/*
* (non-Javadoc)
*
* @see org.openntf.domino.types.SessionDescendant#getAncestorSession()
*/
@Override
public final Session getAncestorSession() {
return getAncestorDatabase().getAncestorSession();
}
/*
* (non-Javadoc)
*
* @see org.openntf.domino.types.Design#getDocument()
*/
@Override
public final Document getDocument() {
if (!StringUtil.isEmpty(universalId_)) {
return database_.getDocumentByUNID(universalId_);
}
return null;
}
/*
* (non-Javadoc)
*
* @see org.openntf.domino.types.Design#getNoteID()
*/
@Override
public final String getUniversalID() {
return universalId_;
}
/*
* (non-Javadoc)
*
* @see org.openntf.domino.types.Design#getUniversalID()
*/
@Override
public final String getNoteID() {
XMLNode node = getDxl().selectSingleNode("//noteinfo");
if (node != null) {
return node.getAttribute("noteid");
}
return "";
}
protected final XMLDocument getDxl() {
if (dxl_ == null) {
DxlExporter exporter = getAncestorSession().createDxlExporter();
exporter.setOutputDOCTYPE(false);
exporter.setForceNoteFormat(enforceRawFormat());
// TODO: You will get an exporter error, if the design is protected. This should be handled correctly
String xml = doExport(exporter);
loadDxl(xml);
if (dxl_ == null)
throw new IllegalStateException(getClass().getSimpleName() + ": Could not load DXL");
XMLNode docRoot = getDocumentElement();
if (docRoot.getNodeName() == "note") {
if (!enforceRawFormat()) {
throw new UnsupportedOperationException(getClass().getSimpleName()
+ ": got note in RAW format. this was not expected. NoteID " + (document_ == null ? "" : document_.getNoteID()));
}
dxlFormat_ = DxlFormat.RAWNOTE;
} else {
if (enforceRawFormat()) {
throw new UnsupportedOperationException(getClass().getSimpleName() + ": Raw format was enforced, but we got a "
+ docRoot.getNodeName());
}
dxlFormat_ = DxlFormat.DXL;
}
}
return dxl_;
}
protected String doExport(final DxlExporter exporter) {
return exporter.exportDxl(document_);
}
protected final XMLNode getDocumentElement() {
return getDxl().getDocumentElement();
}
protected void loadDxl(final String xml) {
dxl_ = new XMLDocument();
try {
dxl_.loadString(xml);
} catch (SAXException e) {
DominoUtils.handleException(e);
} catch (IOException e) {
DominoUtils.handleException(e);
} catch (ParserConfigurationException e) {
DominoUtils.handleException(e);
}
}
protected final void loadDxl(final InputStream is) {
dxl_ = new XMLDocument();
try {
dxl_.loadInputStream(is);
} catch (SAXException e) {
DominoUtils.handleException(e);
} catch (IOException e) {
DominoUtils.handleException(e);
} catch (ParserConfigurationException e) {
DominoUtils.handleException(e);
}
}
@Override
public boolean save() {
DxlImporter importer = getAncestorSession().createDxlImporter();
importer.setDesignImportOption(DxlImporter.DesignImportOption.REPLACE_ELSE_CREATE);
importer.setReplicaRequiredForReplaceOrUpdate(false);
Database database = getAncestorDatabase();
try {
importer.importDxl(getDxl().getXml(null), database);
} catch (IOException e) {
DominoUtils.handleException(e);
if (importer != null) {
System.out.println(importer.getLog());
}
return false;
}
// Reset the DXL so that it can pick up new noteinfo
setDocument(database.getDocumentByID(importer.getFirstImportedNoteID()));
return true;
}
public final String getDesignTemplateName() {
return getItemValueString("$Class");
}
public final void setDesignTemplateName(final String designTemplateName) {
setItemValue("$Class", designTemplateName, FLAG_SUMMARY); // Summary, don't sign
}
/**
* Sets the <code>value</code> of the Item with the given <code>itemName</code>
*
* @param itemName
* the itemName
* @param value
* the value to set
*/
public final void setItemValue(final String itemName, final Object value, final Set<ItemFlag> flags) {
getDxlFormat(true); // load DXL before modification
XMLNode node = getDxlNode("//item[@name='" + XMLDocument.escapeXPathValue(itemName) + "']");
if (node == null) {
node = getDxl().selectSingleNode("/*").addChildElement("item");
node.setAttribute("name", itemName);
} else {
node.removeChildren();
}
if (flags.contains(ItemFlag._SIGN)) {
node.setAttribute("sign", "true");
} else {
node.removeAttribute("sign");
}
if (flags.contains(ItemFlag._SUMMARY)) {
node.removeAttribute("summary");
} else {
node.setAttribute("summary", "false");
}
if (value instanceof Iterable) {
Object first = ((Iterable<?>) value).iterator().next();
XMLNode list = node.addChildElement(first instanceof Number ? "numberlist" : "textlist");
for (Object val : (Iterable<?>) value) {
appendItemValueNode(list, val);
}
} else {
appendItemValueNode(node, value);
}
}
/*
* Helper
*/
private final void appendItemValueNode(final XMLNode node, final Object value) {
XMLNode child;
if (value instanceof Number) {
child = node.addChildElement("number");
} else {
child = node.addChildElement("text");
}
child.setText(String.valueOf(value));
}
/**
* Reads the given item name and returns the containing string (can only read number and text items)
*
* @param itemName
* the ItemName
* @return the values as String (if ItemName is a multi value item, the first value is returned)
*/
public final String getItemValueString(final String itemName) {
if (dxlFormat_ == DxlFormat.NONE)
return document_.getItemValueString(itemName);
XMLNode node = getDxlNode("//item[@name='" + XMLDocument.escapeXPathValue(itemName) + "']");
if (node != null) {
node = node.selectSingleNode(".//number | .//text");
if (node != null)
return node.getText();
}
return "";
}
/**
* Reads the given item name and returns the containing strings, (can only read number and text items)
*
* @param itemName
* the ItemName
* @param delimiter
* the delimiter for multi values
* @return the values as String
*/
public final String getItemValueStrings(final String itemName, final String delimiter) {
StringBuffer sb = new StringBuffer();
if (dxlFormat_ == DxlFormat.NONE) {
for (Object value : document_.getItemValue(itemName)) {
if (sb.length() > 0)
sb.append(delimiter);
sb.append(value);
}
} else {
XMLNode node = getDxlNode("//item[@name='" + XMLDocument.escapeXPathValue(itemName) + "']");
if (node != null) {
List<XMLNode> nodes = node.selectNodes(".//number | .//text");
for (XMLNode child : nodes) {
if (sb.length() > 0)
sb.append(delimiter);
sb.append(child.getText());
}
}
}
return sb.toString();
}
/*
* (non-Javadoc)
*
* @see org.openntf.domino.design.FileResource#getItemNames()
*/
@Override
public Collection<String> getItemNames() {
Collection<String> result = new TreeSet<String>();
for (XMLNode node : getDxl().selectNodes("//item")) {
String itemName = node.getAttribute("name");
if (!itemName.isEmpty()) {
result.add(itemName);
}
}
return result;
}
/**
* Reads the given item name and returns the containing string (can only read number and text items)
*
* @param itemName
* the ItemName
* @return the values as List<Object> (text entries are returned as String, number entries as Double)
*/
public final List<Object> getItemValue(final String itemName) {
if (dxlFormat_ == DxlFormat.NONE)
return document_.getItemValue(itemName);
List<Object> result = new ArrayList<Object>();
XMLNode node = getDxlNode("//item[@name='" + XMLDocument.escapeXPathValue(itemName) + "']");
if (node != null) {
List<XMLNode> nodes = node.selectNodes(".//number | .//text");
for (XMLNode child : nodes) {
if (child.getNodeName().equals("number")) {
result.add(Double.parseDouble(child.getText()));
} else {
result.add(child.getText());
}
}
}
return result;
}
/**
* Reads the given item name and returns the containing string (can only read number and text items)
*
* @param itemName
* the ItemName
* @return the values as List<String>
*/
public final List<String> getItemValueStrings(final String itemName) {
if (dxlFormat_ == DxlFormat.NONE)
return document_.getItemValues(itemName, String.class);
List<String> result = new ArrayList<String>();
XMLNode node = getDxlNode("//item[@name='" + XMLDocument.escapeXPathValue(itemName) + "']");
if (node != null) {
List<XMLNode> nodes = node.selectNodes(".//number | .//text");
for (XMLNode child : nodes) {
result.add(child.getText());
}
}
return result;
}
/**
* Returns the XML node that mathches the given XPath expression
*
* @param xpathString
* the XPath
* @return the XMLNode
*/
public final XMLNode getDxlNode(final String xpathString) {
return getDxl().selectSingleNode(xpathString);
}
// ----------- Serializable stuff ------------------
/**
* Called, when deserializing the object
*
* @param in
* @throws IOException
* @throws ClassNotFoundException
*/
private void readObject(final java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// TODO: Reattach the database?
}
/**
* Called, when serializing the object. Needed to support lazy dxl initalization representation
*
* @param out
* @throws IOException
*/
private void writeObject(final java.io.ObjectOutputStream out) throws IOException {
getDxl();
out.defaultWriteObject();
}
public Date getDocLastModified() {
return lastModified_;
}
}