/** * Copyright (C) 2010-2017 Structr GmbH * * This file is part of Structr <http://structr.org>. * * Structr is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * Structr is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Structr. If not, see <http://www.gnu.org/licenses/>. */ package org.structr.files.ssh.filesystem.path.page; import java.io.IOException; import java.nio.channels.FileChannel; import java.nio.file.AccessDeniedException; import java.nio.file.CopyOption; import java.nio.file.DirectoryNotEmptyException; import java.nio.file.DirectoryStream; import java.nio.file.FileAlreadyExistsException; import java.nio.file.InvalidPathException; import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.FileAttributeView; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.structr.api.util.Iterables; import org.structr.common.PropertyView; import org.structr.common.error.FrameworkException; import org.structr.common.fulltext.Indexable; import org.structr.core.app.App; import org.structr.core.app.StructrApp; import org.structr.core.graph.NodeAttribute; import org.structr.core.graph.Tx; import org.structr.core.property.BooleanProperty; import org.structr.core.property.PropertyKey; import org.structr.files.ssh.filesystem.StructrFilesystem; import org.structr.files.ssh.filesystem.StructrPath; import org.structr.files.ssh.filesystem.path.components.StructrNonexistingComponentPath; import org.structr.files.ssh.filesystem.path.graph.StructrNodePropertyPath; import org.structr.web.entity.dom.DOMElement; import org.structr.web.entity.dom.DOMNode; import org.structr.web.entity.dom.Page; import org.structr.web.entity.dom.ShadowDocument; import org.structr.web.entity.dom.Template; import org.structr.web.entity.dom.relationship.DOMChildren; import org.w3c.dom.Document; /** * */ public class StructrDOMNodePath extends StructrPath { private static final Set<String> blacklist = new LinkedHashSet<>(Arrays.asList(new String[] { "type", "pageId", "parent", "children", "childrenIds", "syncedNodes", "mostUsedTags", "isDOMNode", "isPage", "createdDate", "createdBy", "lastModifiedDate", "linkingElements", "deleted", "hidden", "isContent", "version" } )); private static final Logger logger = LoggerFactory.getLogger(StructrDOMNodePath.class.getName()); private Page ownerDocument = null; private DOMNode parentNode = null; private DOMNode domNode = null; private String uuid = null; public StructrDOMNodePath(final StructrFilesystem fs, final StructrPath parent, final Page ownerDocument, final DOMNode parentNode, final DOMNode node, final String name) { super(fs, parent, name); this.ownerDocument = ownerDocument; this.parentNode = parentNode; this.domNode = node; if (node != null) { this.uuid = node.getUuid(); } } @Override public DirectoryStream<Path> getDirectoryStream(DirectoryStream.Filter<? super Path> filter) { // this is called when a directory is opened, so we can safely begin our transaction here final Tx tx = StructrApp.getInstance(fs.getSecurityContext()).tx(); return new DirectoryStream() { boolean closed = false; @Override public Iterator iterator() { if (!closed) { final StructrPath.HiddenFileEntry hiddenFiles = StructrPath.HIDDEN_PROPERTY_FILES.get(domNode.getUuid()); final List<StructrPath> nodes = new LinkedList<>(); int pos = 0; try { for (final DOMNode child : domNode.treeGetChildren()) { final int domPosition = getDomPosition(child, pos); // store position just in case.. child.setProperty(DOMNode.domSortPosition, domPosition); // add node to return set nodes.add(new StructrDOMNodePath(fs, StructrDOMNodePath.this, ownerDocument, domNode, child, getName(null, child, pos))); pos += 1; } final Set<PropertyKey> exportedKeys = new LinkedHashSet<>(); Iterables.addAll(exportedKeys, domNode.getPropertyKeys(PropertyView.Ui)); Iterables.addAll(exportedKeys, domNode.getPropertyKeys(PropertyView.Html)); hidePropertyKeys(hiddenFiles); for (final PropertyKey key : exportedKeys) { final Object value = domNode.getProperty(key); Object defaultValue = key.defaultValue(); // boolean properties with default value "false" should not be visible if (key instanceof BooleanProperty) { defaultValue = Boolean.FALSE; } // do not export properties that return null or their default value if (value != null && !(value.equals(defaultValue))) { final StructrPath path = resolveStructrPath(key.jsonName()); if (path != null) { if (hiddenFiles != null) { hiddenFiles.remove(key.jsonName()); } nodes.add(path); } } } } catch (FrameworkException fex) { logger.warn("", fex); } return nodes.iterator(); } return Collections.emptyIterator(); } @Override public void close() throws IOException { closed = true; try { tx.success(); tx.close(); } catch (FrameworkException fex) { logger.warn("", fex); } } }; } @Override public FileChannel newFileChannel(final Set<? extends OpenOption> options, final FileAttribute<?>... attrs) throws IOException { throw new UnsupportedOperationException("Not supported."); } @Override public void createDirectory(final FileAttribute<?>... attrs) throws IOException { // create a new directory with the name of this path final App app = StructrApp.getInstance(fs.getSecurityContext()); final ElementName en = new ElementName(name); if (!en.hasTagName() || !en.hasPosition()) { throw new InvalidPathException(name, "New element needs tag and position, e.g. 001-div"); } try (final Tx tx = app.tx()) { if (ownerDocument != null) { int index = 0; if (parentNode != null) { // check if parent node already has a child at the given position for (final DOMNode child : parentNode.treeGetChildren()) { final int childPosition = getDomPosition(child, index++); if (childPosition == en.getPosition()) { throw new InvalidPathException(name, "A child with position " + childPosition + " already exists."); } } } // all clear, create new child final DOMNode newChild = createNode(ownerDocument, en.getTagName()); final int newPosition = en.getPosition(); if (parentNode != null) { insertDOMNodeAt(newChild, newPosition); } if (newChild instanceof DOMNode) { this.domNode = newChild; // Add all properties to the list of "hidden" properties so that copying // a directory does not fail with "file already exists". Properties will // be "activated" when the user requests a directory listing. final StructrPath.HiddenFileEntry entry = new StructrPath.HiddenFileEntry(); for (final PropertyKey key : domNode.getPropertyKeys(PropertyView.Ui)) { entry.add(key.jsonName()); } for (final PropertyKey key : domNode.getPropertyKeys(PropertyView.Html)) { entry.add(key.jsonName()); } hidePropertyKeys(entry); StructrPath.HIDDEN_PROPERTY_FILES.put(domNode.getUuid(), entry); } } tx.success(); } catch (FrameworkException fex) { logger.warn("", fex); } } @Override public void delete() throws IOException { final App app = StructrApp.getInstance(fs.getSecurityContext()); try (final Tx tx = app.tx()) { if (domNode.treeGetChildren().isEmpty()) { if (parentNode != null) { parentNode.removeChild(domNode); } app.delete(domNode); this.domNode = null; } else { throw new DirectoryNotEmptyException(name); } tx.success(); } catch (FrameworkException fex) { logger.warn("", fex); } } @Override public StructrPath resolveStructrPath(final String pathComponent) { if (blacklist.contains(pathComponent)) { return null; } if ("name".equals(pathComponent)) { // pages don't have a settable name property if (domNode instanceof Page) { return null; } // shared components have their names set using the directory name if (domNode.getOwnerDocument() instanceof ShadowDocument) { return null; } } if (domNode != null) { // special handling for data-* attributes because there are no property keys // stored in the configuration provider if (pathComponent != null && pathComponent.startsWith("data-")) { final PropertyKey key = StructrApp.getConfiguration().getPropertyKeyForJSONName(domNode.getClass(), pathComponent); if (key != null) { return new StructrNodePropertyPath(fs, this, domNode, key); } } final PropertyKey key = StructrApp.getConfiguration().getPropertyKeyForJSONName(domNode.getClass(), pathComponent, false); if (key != null) { return new StructrNodePropertyPath(fs, this, domNode, key); } else { final ElementName en = new ElementName(pathComponent); final int pos = en.getPosition(); if (pos >= 0) { int childPosition = 0; DOMNode node = null; // iterate over children, find node with given position for (final DOMNode child : domNode.treeGetChildren()) { final int domSortPosition = getDomPosition(child, childPosition); if (pos == domSortPosition) { node = child; break; } // advance default child position childPosition += 1; } return new StructrDOMNodePath(fs, StructrDOMNodePath.this, ownerDocument, domNode, node, pathComponent); } } } return new StructrDOMNodePath(fs, this, ownerDocument, domNode, null, pathComponent); } @Override public Map<String, Object> getAttributes(final String attributes, final LinkOption... options) throws IOException { if (domNode != null) { return new StructrDOMAttributes(fs.getSecurityContext(), domNode).toMap(attributes); } throw new NoSuchFileException(toString()); } @Override public <T extends BasicFileAttributes> T getAttributes(Class<T> type, LinkOption... options) throws IOException { if (domNode != null) { return (T)new StructrDOMAttributes(fs.getSecurityContext(), domNode); } throw new NoSuchFileException(toString()); } @Override public void copy(final Path target, final CopyOption... options) throws IOException { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @Override public void move(final Path target, final CopyOption... options) throws IOException { if (target instanceof StructrDOMNodePath) { final App app = StructrApp.getInstance(fs.getSecurityContext()); final StructrDOMNodePath targetPath = (StructrDOMNodePath)target; try (final Tx tx = app.tx()) { if (targetPath.parentNode != null && targetPath.parentNode.getUuid().equals(parentNode.getUuid())) { final String targetName = target.getFileName().toString(); final ElementName targetElementName = new ElementName(targetName); final ElementName sourceElementName = new ElementName(name); if (targetElementName.getTagName().equals(sourceElementName.getTagName())) { final int newPosition = targetElementName.getPosition(); // remove child from current position parentNode.removeChild(domNode); // insert at new position insertDOMNodeAt(domNode, newPosition); } else { throw new InvalidPathException(targetName, "Cannot change element type."); } } else { } tx.success(); } catch (FrameworkException fex) { logger.warn("Unable to move {} to {}: {}", new Object[] { this, target, fex.getMessage() }); } } else if (target instanceof StructrNonexistingComponentPath & domNode != null) { // don't move is source node is not a shared component (allow rename but not move) if (!(domNode.getOwnerDocument() instanceof ShadowDocument)) { throw new AccessDeniedException("Cannot move DOM node to shared components"); } else { final String targetName = target.getFileName().toString(); final int pos = targetName.indexOf("-"); String componentName = null; String componentTag = null; if (pos == -1) { throw new InvalidPathException(targetName, "Component name must contain tag and name, e.g. div-test"); } componentTag = targetName.substring(0, pos); componentName = targetName.substring(pos+1); if (componentName.isEmpty() || componentTag.isEmpty()) { throw new InvalidPathException(targetName, "Component name must contain tag and name, e.g. div-test"); } final App app = StructrApp.getInstance(fs.getSecurityContext()); try (final Tx tx = app.tx()) { final ShadowDocument doc = StructrApp.getInstance(fs.getSecurityContext()).nodeQuery(ShadowDocument.class).includeDeletedAndHidden().getFirst(); for (final DOMNode child : doc.getProperty(Page.elements)) { if (!child.hasIncomingRelationships(DOMChildren.class)) { if (componentName.equals(child.getName()) && componentTag.equals(child.getProperty(DOMElement.tag))) { throw new FileAlreadyExistsException(targetName); } } } domNode.setProperty(DOMElement.name, componentName); tx.success(); } catch (FrameworkException fex) { logger.warn("", fex); } } } } @Override public void setAttribute(final String attribute, final Object value, final LinkOption... options) throws IOException { } @Override public <V extends FileAttributeView> V getFileAttributeView(final Class<V> type, final LinkOption... options) throws IOException { return (V)getAttributes((Class)null, options); } @Override public boolean isSameFile(final Path path2) throws IOException { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @Override public void enablePropertyFile(final String name) { if (uuid != null) { final StructrPath.HiddenFileEntry entry = StructrPath.HIDDEN_PROPERTY_FILES.get(uuid); if (entry != null) { if (name.startsWith("data-")) { entry.addDynamicWithValue(name); } entry.remove(name); if (entry.isEmpty()) { StructrPath.HIDDEN_PROPERTY_FILES.remove(uuid); } } } } @Override public boolean hasPropertyFile(final String name) { boolean result = true; if (uuid != null) { // this code looks for the UUID of a DOMNode in the set of "currently // created DOMNodes" and returns whether the property with the given // name has been accessed yet. final StructrPath.HiddenFileEntry entry = StructrPath.HIDDEN_PROPERTY_FILES.get(uuid); if (entry != null) { if (entry.has(name)) { result = false; } else if (name.startsWith("data-")) { result = entry.hasDynamicWithValue(name); } } } // if the node is not in the set of nodes in the process of being created, return true return result; } // ----- private methods ----- private void insertDOMNodeAt(final DOMNode newChild, int newPosition) throws FrameworkException { DOMNode refChild = null; int position = 0; // find element with the lowest dom position greater than the new element's pos for (final DOMNode child : parentNode.treeGetChildren()) { final int domPosition = getDomPosition(child, position++); if (domPosition > newPosition && refChild == null) { refChild = child; } // store position in child if (child.getProperty(DOMNode.domSortPosition) == null) { child.setProperty(DOMNode.domSortPosition, domPosition); } } if (refChild != null) { // position was found, insert before parentNode.insertBefore(newChild, refChild); } else { // new position is larger than any existing position // or parent has no children yet => append new child parentNode.appendChild(newChild); } // store position in node newChild.setProperty(DOMNode.domSortPosition, newPosition); } private DOMNode createNode(final Document doc, final String tagName) { switch (tagName) { case "content": return (DOMNode)doc.createTextNode("#text"); case "comment": return (DOMNode)doc.createComment("#comment"); case "template": Template newNode = null; try { newNode = StructrApp.getInstance().create(Template.class, new NodeAttribute<>(Template.parent, (Page)doc), new NodeAttribute<>(Template.ownerDocument, (Page)doc), new NodeAttribute<>(Template.content, "#template") ); } catch (FrameworkException fex) { logger.warn("Unable to create new template node", fex); } return newNode; } return (DOMNode)doc.createElement(tagName); } private void hidePropertyKeys(final StructrPath.HiddenFileEntry hiddenKeys) { if (hiddenKeys != null) { hiddenKeys.add(DOMNode.visibleToAuthenticatedUsers.jsonName()); hiddenKeys.add(DOMNode.visibleToPublicUsers.jsonName()); hiddenKeys.add(DOMNode.sharedComponent.jsonName()); hiddenKeys.add(DOMNode.renderDetails.jsonName()); hiddenKeys.add(Indexable.contentType.jsonName()); } } // ----- nested classes ----- public static class ElementName { private int position = -1; private String src = null; private String tag = null; public ElementName(final String src) { this.src = src; parsePositionAndName(src); } @Override public String toString() { final StringBuilder buf = new StringBuilder(); buf.append(src); buf.append(" = ElementName("); buf.append(tag); buf.append(", "); buf.append(position); buf.append(")"); return buf.toString(); } public int getPosition() { return position; } public boolean hasPosition() { return position >= 0; } public String getTagName() { return tag; } public boolean hasTagName() { return tag != null; } private void parsePositionAndName(final String src) { // split on "-" first final int pos = src.indexOf("-"); if (pos >= 0) { final String positionSrc = src.substring(0, pos); final String nameSrc = src.substring(pos+1); // no position, try to parse the rest from first part this.position = getPosition(positionSrc); if (this.position >= 0) { this.tag = nameSrc; } else { logger.warn("Unable to extract position from {}: invalid source name", src); } } else { // fallback: no position this.tag = src; } } private int getPosition(final String src) { try { return Integer.valueOf(src); } catch (Throwable t) {} return -1; } } }