package com.thinkbiganalytics.metadata.modeshape.extension; /*- * #%L * thinkbig-metadata-modeshape * %% * Copyright (C) 2017 ThinkBig Analytics * %% * 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. * #L% */ import com.thinkbiganalytics.metadata.api.extension.ExtensibleType; import com.thinkbiganalytics.metadata.api.extension.ExtensibleType.ID; import com.thinkbiganalytics.metadata.api.extension.ExtensibleTypeBuilder; import com.thinkbiganalytics.metadata.api.extension.ExtensibleTypeProvider; import com.thinkbiganalytics.metadata.api.extension.FieldDescriptor; import com.thinkbiganalytics.metadata.api.extension.FieldDescriptor.Type; import com.thinkbiganalytics.metadata.api.extension.FieldDescriptorBuilder; import com.thinkbiganalytics.metadata.modeshape.JcrMetadataAccess; import com.thinkbiganalytics.metadata.modeshape.MetadataRepositoryException; import com.thinkbiganalytics.metadata.modeshape.TypeAlreadyExistsException; import com.thinkbiganalytics.metadata.modeshape.support.JcrUtil; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.jcr.ItemNotFoundException; import javax.jcr.Node; import javax.jcr.PathNotFoundException; import javax.jcr.PropertyType; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.UnsupportedRepositoryOperationException; import javax.jcr.nodetype.NoSuchNodeTypeException; import javax.jcr.nodetype.NodeType; import javax.jcr.nodetype.NodeTypeIterator; import javax.jcr.nodetype.NodeTypeManager; import javax.jcr.nodetype.NodeTypeTemplate; import javax.jcr.nodetype.PropertyDefinition; import javax.jcr.nodetype.PropertyDefinitionTemplate; public class JcrExtensibleTypeProvider implements ExtensibleTypeProvider { @Override public ID resolve(final Serializable ser) { return (ser instanceof ID) ? (ID) ser : new JcrExtensibleType.TypeId(ser); } @Override public ExtensibleType getType(final ID id) { final JcrExtensibleType.TypeId typeId = (JcrExtensibleType.TypeId) id; final Session session = getSession(); try { final Node typeNode = session.getNodeByIdentifier(typeId.getIdValue()); final NodeType nodeType = session.getWorkspace().getNodeTypeManager().getNodeType(typeNode.getName()); return new JcrExtensibleType(typeNode, nodeType); } catch (ItemNotFoundException e) { return null; } catch (NoSuchNodeTypeException | PathNotFoundException e) { return null; } catch (RepositoryException e) { throw new MetadataRepositoryException("Failure retriving exstenible type with ID: " + id, e); } } @Override public ExtensibleType getType(final String name) { final Session session = getSession(); final String typeName = ensureTypeName(name); try { final NodeType nodeType = session.getWorkspace().getNodeTypeManager().getNodeType(typeName); final Node typeNode = session.getRootNode().getNode(ExtensionsConstants.TYPES + "/" + typeName); return new JcrExtensibleType(typeNode, nodeType); } catch (NoSuchNodeTypeException | PathNotFoundException e) { return null; } catch (RepositoryException e) { throw new MetadataRepositoryException("Failed to lookup extensible type: " + name, e); } } @Override public List<ExtensibleType> getTypes() { return getTypesList("tba:extensibleEntity"); } @Override public List<ExtensibleType> getTypes(final ExtensibleType type) { return getTypesList(type.getName()); } @Override public boolean deleteType(final ID id) { final Session session = getSession(); final JcrExtensibleType.TypeId typeId = (JcrExtensibleType.TypeId) id; try { final Node typeNode = session.getNodeByIdentifier(typeId.getIdValue()); session.getWorkspace().getNodeTypeManager().unregisterNodeType(typeNode.getName()); session.getRootNode().getNode(ExtensionsConstants.TYPES + "/" + typeNode.getName()).remove(); return true; } catch (ItemNotFoundException | NoSuchNodeTypeException | NullPointerException e) { // KYLO-8: Ignore NPE caused by unregistering a node type return true; } catch (UnsupportedRepositoryOperationException e) { return false; } catch (RepositoryException e) { throw new MetadataRepositoryException("Unable to retrieve extensible type: " + id, e); } } /** * Return the Property names and types for a given NodeType (i.e. pass in tba:feed). */ @Override public Set<FieldDescriptor> getPropertyDescriptors(final String nodeType) { return getType(nodeType).getFieldDescriptors(); } @Nonnull @Override public ExtensibleTypeBuilder buildType(@Nonnull final String name) { return new TypeBuilder(ensureTypeName(name), null); } @Nonnull @Override public ExtensibleTypeBuilder updateType(@Nonnull final ID id) { try { final JcrExtensibleType.TypeId typeId = (JcrExtensibleType.TypeId) id; final Node typeNode = getSession().getNodeByIdentifier(typeId.getIdValue()); return new TypeBuilder(typeNode.getName(), typeNode); } catch (RepositoryException e) { throw new MetadataRepositoryException("Unable to retrieve extensible type: " + id, e); } } private List<ExtensibleType> getTypesList(final String typeName) { final Session session = getSession(); try { final List<ExtensibleType> list = new ArrayList<>(); final NodeTypeManager typeMgr = session.getWorkspace().getNodeTypeManager(); final NodeTypeIterator typeItr = typeMgr.getPrimaryNodeTypes(); final NodeType extensibleType = typeMgr.getNodeType(typeName); while (typeItr.hasNext()) { final NodeType nodeType = (NodeType) typeItr.next(); if (nodeType.isNodeType(extensibleType.getName()) && !nodeType.equals(extensibleType)) { final Node typeNode = session.getRootNode().getNode(ExtensionsConstants.TYPES + "/" + nodeType.getName()); list.add(new JcrExtensibleType(typeNode, nodeType)); } } return list; } catch (RepositoryException e) { throw new MetadataRepositoryException("Failed to lookup all extensible types", e); } } public int asCode(final Type type) { switch (type) { case BOOLEAN: return PropertyType.BOOLEAN; case DOUBLE: return PropertyType.DOUBLE; case INTEGER: return PropertyType.LONG; case LONG: return PropertyType.LONG; case STRING: return PropertyType.STRING; case ENTITY: return PropertyType.REFERENCE; case WEAK_REFERENCE: return PropertyType.WEAKREFERENCE; default: return PropertyType.STRING; } } private Session getSession() { return JcrMetadataAccess.getActiveSession(); } /** * Builds the type defined by the specified builder. * * @param builder the builder for the type * @return the type * @throws MetadataRepositoryException if the repository is unavailable * @throws TypeAlreadyExistsException if a type with the same name already exists */ @Nonnull private ExtensibleType buildType(@Nonnull final TypeBuilder builder) { try { // Remove existing type and subgraph if (builder.node != null) { builder.node.remove(); } // Get or create the type node final Session session = getSession(); final Node typeNode; if (!session.nodeExists(JcrUtil.path(session.getRootNode().getPath(), ExtensionsConstants.TYPES + "/" + builder.name).toString())) { typeNode = session.getRootNode().addNode(ExtensionsConstants.TYPES + "/" + builder.name, ExtensionsConstants.TYPE_DESCRIPTOR_TYPE); } else { throw new TypeAlreadyExistsException(builder.name); } // Update type metadata if (builder.displayName != null) { typeNode.setProperty(JcrExtensibleType.NAME, builder.displayName); } if (builder.description != null) { typeNode.setProperty(JcrExtensibleType.DESCRIPTION, builder.description); } // Update type definition final NodeTypeManager typeMgr = session.getWorkspace().getNodeTypeManager(); final NodeTypeTemplate nodeTemplate = typeMgr.createNodeTypeTemplate(); nodeTemplate.setName(builder.name); if (builder.supertype != null) { final JcrExtensibleType superImpl = (JcrExtensibleType) builder.supertype; final String supername = superImpl.getJcrName(); nodeTemplate.setDeclaredSuperTypeNames(new String[]{ExtensionsConstants.EXTENSIBLE_ENTITY_TYPE, supername}); } else { nodeTemplate.setDeclaredSuperTypeNames(new String[]{ExtensionsConstants.EXTENSIBLE_ENTITY_TYPE}); } // Update field definitions @SuppressWarnings("unchecked") final List<PropertyDefinitionTemplate> propertyDefinitionTemplates = nodeTemplate.getPropertyDefinitionTemplates(); for (final FieldBuilder bldr : builder.fieldBuilders) { // Create field PropertyDefinitionTemplate propDef = typeMgr.createPropertyDefinitionTemplate(); propDef.setName(bldr.name); propDef.setRequiredType(asCode(bldr.type)); propDef.setMandatory(bldr.required); propDef.setMultiple(bldr.collection); propertyDefinitionTemplates.add(propDef); // Set field metadata Node fieldNode = typeNode.addNode(bldr.name, ExtensionsConstants.FIELD_DESCRIPTOR_TYPE); for (final Map.Entry<String, String> entry : bldr.metadata.entrySet()) { fieldNode.setProperty(entry.getKey(), entry.getValue()); } } NodeType nodeType = typeMgr.registerNodeType(nodeTemplate, true); return new JcrExtensibleType(typeNode, nodeType); } catch (RepositoryException e) { throw new MetadataRepositoryException("Failed to create type: " + builder.name, e); } } /** * Ensures that the specified name is a valid type name. * * <p>The {@code tba:} namespace will be added if the name is not already namespaced.</p> * * @param name the name to be checked * @return a valid type name * @throws NullPointerException if the name is {@code null} */ @Nonnull private String ensureTypeName(@Nullable final String name) { if (name == null) { throw new NullPointerException("Type name cannot be null"); } return name.matches("^\\w*:.*") ? name : JcrMetadataAccess.TBA_PREFIX + ":" + name; } /** * An {@link ExtensibleTypeBuilder} that builds JCR {@link NodeType} objects. */ private class TypeBuilder implements ExtensibleTypeBuilder { /** * Field definitions */ @Nonnull private final Set<FieldBuilder> fieldBuilders = new HashSet<>(); /** * Name of the node type */ @Nonnull private final String name; /** * Node containing type definition */ @Nullable private final Node node; /** * Human-readable specification */ @Nullable private String description; /** * Human-readable title */ @Nullable private String displayName; /** * Parent type */ @Nullable private ExtensibleType supertype; /** * Constructs an {@code ExtensibleTypeBuilder} with the specified type name and optional type node. * * @param name the name of the node type * @param node the node containing the type definition, or {@code null} if this is a new type */ TypeBuilder(@Nonnull final String name, @Nullable final Node node) { this.name = name; this.node = node; } @Override public ExtensibleTypeBuilder supertype(final ExtensibleType type) { this.supertype = type; return this; } @Override public ExtensibleTypeBuilder displayName(final String dispName) { this.displayName = dispName; return this; } @Override public ExtensibleTypeBuilder description(String descr) { this.description = descr; return this; } @Override public ExtensibleTypeBuilder addField(String name, Type type) { return new FieldBuilder(this).name(name).type(type).add(); } @Override public FieldDescriptorBuilder field(String name) { return new FieldBuilder(this).name(name); } @Nonnull @Override public ExtensibleType build() { return buildType(this); } } /** * A {@link FieldDescriptorBuilder} that builds JCR {@link PropertyDefinition} objects. */ private class FieldBuilder implements FieldDescriptorBuilder { /** * Metadata for this field */ @Nonnull private final Map<String, String> metadata = new HashMap<>(); /** * Parent type builder */ @Nonnull private final TypeBuilder typeBuilder; /** * Indicates that multiple values are allowed */ private boolean collection; /** * Name of this field */ @Nullable private String name; /** * Indicates that a value is required */ private boolean required; /** * Value type */ private Type type; /** * Constructs a {@code FieldBuilder} with the specified parent type builder. * * @param typeBldr the parent type builder */ FieldBuilder(@Nonnull final TypeBuilder typeBldr) { this.typeBuilder = typeBldr; } @Override public FieldDescriptorBuilder name(final String name) { this.name = name; return this; } @Override public FieldDescriptorBuilder type(final Type type) { this.type = type; return this; } @Override public FieldDescriptorBuilder displayName(final String name) { metadata.put(JcrExtensibleType.NAME, name); return this; } @Override public FieldDescriptorBuilder description(final String descr) { metadata.put(JcrExtensibleType.DESCRIPTION, descr); return this; } @Override public FieldDescriptorBuilder collection(final boolean flag) { this.collection = flag; return this; } @Override public FieldDescriptorBuilder required(final boolean flag) { this.required = flag; return this; } @Nonnull @Override public FieldDescriptorBuilder property(@Nonnull final String name, @Nonnull final String value) { metadata.put(name, value); return this; } @Override public ExtensibleTypeBuilder add() { this.typeBuilder.fieldBuilders.add(this); return this.typeBuilder; } } }