/*
* ModeShape (http://www.modeshape.org)
*
* 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.modeshape.jcr;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.jcr.PropertyType;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.PropertyDefinition;
import javax.jcr.version.OnParentVersionAction;
import org.modeshape.common.annotation.Immutable;
import org.modeshape.common.annotation.NotThreadSafe;
import org.modeshape.jcr.api.query.qom.Operator;
import org.modeshape.jcr.cache.PropertyTypeUtil;
import org.modeshape.jcr.query.PseudoColumns;
import org.modeshape.jcr.query.model.AllNodes;
import org.modeshape.jcr.query.model.SelectorName;
import org.modeshape.jcr.query.model.TypeSystem;
import org.modeshape.jcr.query.validate.ImmutableSchemata;
import org.modeshape.jcr.query.validate.Schemata;
import org.modeshape.jcr.value.Name;
import org.modeshape.jcr.value.NameFactory;
import org.modeshape.jcr.value.NamespaceRegistry;
import org.modeshape.jcr.value.NamespaceRegistry.Namespace;
import org.modeshape.jcr.value.StringFactory;
import org.modeshape.jcr.value.basic.LocalNamespaceRegistry;
/**
* A {@link Schemata} implementation that is constructed from the {@link NodeType}s and {@link PropertyDefinition}s contained
* within a {@link RepositoryNodeTypeManager}. The resulting {@link org.modeshape.jcr.query.validate.Schemata.Table}s will never
* change, so the {@link RepositoryNodeTypeManager} must replace it's cached instance whenever the node types change.
*/
@Immutable
public class NodeTypeSchemata implements Schemata {
protected static final boolean DEFAULT_CAN_CONTAIN_REFERENCES = true;
protected static final boolean DEFAULT_FULL_TEXT_SEARCHABLE = true;
private final Schemata schemata;
private final Map<Integer, String> types;
private final Map<String, String> prefixesByUris = new HashMap<String, String>();
private final boolean includeColumnsForInheritedProperties;
private final boolean includePseudoColumnsInSelectStar;
private final NodeTypes nodeTypes;
private final Map<JcrNodeType, Collection<JcrNodeType>> subtypesByName = new HashMap<JcrNodeType, Collection<JcrNodeType>>();
private final List<JcrPropertyDefinition> pseudoProperties = new ArrayList<JcrPropertyDefinition>();
private final Name[] keyPropertyNames;
NodeTypeSchemata( ExecutionContext context,
NodeTypes nodeTypes,
boolean includeColumnsForInheritedProperties,
boolean includePseudoColumnsInSelectStar ) {
this.includeColumnsForInheritedProperties = includeColumnsForInheritedProperties;
this.includePseudoColumnsInSelectStar = includePseudoColumnsInSelectStar;
this.nodeTypes = nodeTypes;
// Register all the namespace prefixes by URIs ...
for (Namespace namespace : context.getNamespaceRegistry().getNamespaces()) {
this.prefixesByUris.put(namespace.getNamespaceUri(), namespace.getPrefix());
}
// Identify the subtypes for each node type, and do this before we build any views ...
for (JcrNodeType nodeType : nodeTypes.getAllNodeTypes()) {
// For each of the supertypes ...
for (JcrNodeType supertype : nodeType.getTypeAndSupertypes()) {
Collection<JcrNodeType> types = subtypesByName.get(supertype);
if (types == null) {
types = new LinkedList<JcrNodeType>();
subtypesByName.put(supertype, types);
}
types.add(nodeType);
}
}
// Build the schemata for the current node types ...
TypeSystem typeSystem = context.getValueFactories().getTypeSystem();
ImmutableSchemata.Builder builder = ImmutableSchemata.createBuilder(context, nodeTypes);
// Build the fast-search for type names based upon PropertyType values ...
types = new HashMap<Integer, String>();
types.put(PropertyType.BINARY, typeSystem.getBinaryFactory().getTypeName());
types.put(PropertyType.BOOLEAN, typeSystem.getBooleanFactory().getTypeName());
types.put(PropertyType.DATE, typeSystem.getDateTimeFactory().getTypeName());
types.put(PropertyType.DECIMAL, typeSystem.getDecimalFactory().getTypeName());
types.put(PropertyType.DOUBLE, typeSystem.getDoubleFactory().getTypeName());
types.put(PropertyType.LONG, typeSystem.getLongFactory().getTypeName());
types.put(PropertyType.PATH, typeSystem.getStringFactory().getTypeName());
types.put(PropertyType.REFERENCE, typeSystem.getStringFactory().getTypeName());
types.put(PropertyType.WEAKREFERENCE, typeSystem.getStringFactory().getTypeName());
types.put(org.modeshape.jcr.api.PropertyType.SIMPLE_REFERENCE, typeSystem.getStringFactory().getTypeName());
types.put(PropertyType.STRING, typeSystem.getStringFactory().getTypeName());
types.put(PropertyType.NAME, typeSystem.getStringFactory().getTypeName());
types.put(PropertyType.URI, typeSystem.getStringFactory().getTypeName());
// Don't include 'jcr:uuid' in all pseudocolumns, since it should only appear in 'mix:referencable' nodes ...
for (PseudoColumns.Info pseudoColumn : PseudoColumns.allColumnsExceptJcrUuid()) {
pseudoProperties.add(pseudoProperty(context, pseudoColumn.getQualifiedName(), pseudoColumn.getType()));
}
keyPropertyNames = new Name[] {JcrLexicon.UUID, ModeShapeLexicon.ID};
// Create the "ALLNODES" table, which will contain all possible properties ...
addAllNodesTable(builder, context, pseudoProperties, keyPropertyNames);
// Define a view for each node type ...
for (JcrNodeType nodeType : nodeTypes.getAllNodeTypes()) {
addView(builder, context, nodeType);
}
schemata = builder.build();
}
protected JcrPropertyDefinition pseudoProperty( ExecutionContext context,
Name name,
int propertyType ) {
int opv = OnParentVersionAction.IGNORE;
boolean autoCreated = true;
boolean mandatory = true;
boolean isProtected = true;
boolean multiple = false;
boolean fullTextSearchable = false;
boolean queryOrderable = true;
JcrValue[] defaultValues = null;
String[] valueConstraints = new String[] {};
String[] queryOperators = null;
return new JcrPropertyDefinition(context, null, null, name, opv, autoCreated, mandatory, isProtected, defaultValues,
propertyType, valueConstraints, multiple, fullTextSearchable, queryOrderable,
queryOperators);
}
protected JcrNodeType getNodeType( Name nodeTypeName ) {
return nodeTypes.getNodeType(nodeTypeName);
}
protected final void addAllNodesTable( ImmutableSchemata.Builder builder,
ExecutionContext context,
List<JcrPropertyDefinition> additionalProperties,
Name[] keyPropertyNames ) {
NamespaceRegistry registry = context.getNamespaceRegistry();
TypeSystem typeSystem = context.getValueFactories().getTypeSystem();
String tableName = AllNodes.ALL_NODES_NAME.name();
boolean first = true;
Map<String, String> typesForNames = new HashMap<String, String>();
Set<String> fullTextSearchableNames = new HashSet<String>();
for (JcrPropertyDefinition defn : nodeTypes.getAllPropertyDefinitions()) {
if (defn.isResidual()) continue;
Name name = defn.getInternalName();
String columnName = name.getString(registry);
if (first) {
builder.addTable(tableName, columnName);
first = false;
}
org.modeshape.jcr.value.PropertyType requiredType = PropertyTypeUtil.modePropertyTypeFor(defn.getRequiredType());
switch (defn.getRequiredType()) {
case PropertyType.REFERENCE:
break;
case PropertyType.WEAKREFERENCE:
case org.modeshape.jcr.api.PropertyType.SIMPLE_REFERENCE:
case PropertyType.UNDEFINED:
requiredType = org.modeshape.jcr.value.PropertyType.STRING;
break;
}
String type = typeSystem.getDefaultType();
if (defn.getRequiredType() != PropertyType.UNDEFINED) {
type = types.get(defn.getRequiredType());
}
assert type != null;
String previousType = typesForNames.put(columnName, type);
if (previousType != null && !previousType.equals(type)) {
// There are two property definitions with the same name but different types, so we need to find a common type ...
type = typeSystem.getCompatibleType(previousType, type);
}
boolean fullTextSearchable = fullTextSearchableNames.contains(columnName) || defn.isFullTextSearchable();
if (fullTextSearchable) fullTextSearchableNames.add(columnName);
// Add (or overwrite) the column ...
boolean orderable = defn.isQueryOrderable();
Set<Operator> operators = operatorsFor(defn);
Object minimum = defn.getMinimumValue();
Object maximum = defn.getMaximumValue();
builder.addColumn(tableName, columnName, type, requiredType, fullTextSearchable, orderable, minimum, maximum,
operators);
}
if (additionalProperties != null) {
boolean fullTextSearchable = false;
for (JcrPropertyDefinition defn : additionalProperties) {
Name name = defn.getInternalName();
String columnName = name.getString(registry);
assert defn.getRequiredType() != PropertyType.UNDEFINED;
String type = types.get(defn.getRequiredType());
assert type != null;
String previousType = typesForNames.put(columnName, type);
if (previousType != null && !previousType.equals(type)) {
// There are two property definitions with the same name but different types, so we need to find a common type
// ...
type = typeSystem.getCompatibleType(previousType, type);
}
// Add (or overwrite) the column ...
boolean orderable = defn.isQueryOrderable();
Set<Operator> operators = operatorsFor(defn);
Object minimum = defn.getMinimumValue();
Object maximum = defn.getMaximumValue();
org.modeshape.jcr.value.PropertyType requiredType = PropertyTypeUtil.modePropertyTypeFor(defn.getRequiredType());
builder.addColumn(tableName, columnName, type, requiredType, fullTextSearchable, orderable, minimum, maximum,
operators);
if (!includePseudoColumnsInSelectStar) {
builder.excludeFromSelectStar(tableName, columnName);
}
}
}
if (keyPropertyNames != null) {
StringFactory strings = context.getValueFactories().getStringFactory();
for (Name name : keyPropertyNames) {
// Add a key for each key property ...
builder.addKey(tableName, strings.create(name));
}
}
}
protected Set<Operator> operatorsFor( JcrPropertyDefinition defn ) {
String[] ops = defn.getAvailableQueryOperators();
if (ops == null || ops.length == 0) return EnumSet.allOf(Operator.class);
Set<Operator> result = new HashSet<Operator>();
for (String symbol : ops) {
Operator op = JcrPropertyDefinition.operatorFromSymbol(symbol);
assert op != null;
result.add(op);
}
return result;
}
protected final void addView( ImmutableSchemata.Builder builder,
ExecutionContext context,
JcrNodeType nodeType ) {
NamespaceRegistry registry = context.getNamespaceRegistry();
if (!nodeType.isQueryable()) {
// The node type is defined as not queryable, so skip it ...
return;
}
String tableName = nodeType.getName();
JcrPropertyDefinition[] defns = null;
if (includeColumnsForInheritedProperties) {
defns = nodeType.getPropertyDefinitions();
} else {
defns = nodeType.getDeclaredPropertyDefinitions();
}
// Create the SQL statement ...
StringBuilder viewDefinition = new StringBuilder("SELECT ");
boolean hasResidualProperties = false;
boolean first = true;
for (JcrPropertyDefinition defn : defns) {
if (defn.isResidual()) {
hasResidualProperties = true;
continue;
}
// if (defn.isMultiple()) continue;
Name name = defn.getInternalName();
String columnName = name.getString(registry);
if (first) first = false;
else viewDefinition.append(',');
viewDefinition.append('[').append(columnName).append(']');
if (!defn.isQueryOrderable()) {
builder.markOrderable(tableName, columnName, false);
}
builder.markOperators(tableName, columnName, operatorsFor(defn));
}
// Add the pseudo-properties ...
for (JcrPropertyDefinition defn : pseudoProperties) {
Name name = defn.getInternalName();
String columnName = name.getString(registry);
if (first) first = false;
else viewDefinition.append(',');
viewDefinition.append('[').append(columnName).append(']');
builder.markOperators(tableName, columnName, operatorsFor(defn));
}
if (first) {
// All the properties were skipped ...
return;
}
viewDefinition.append(" FROM ").append(AllNodes.ALL_NODES_NAME).append(" AS [").append(tableName).append(']');
// The 'nt:base' node type will have every single object in it, so we don't need to add the type criteria ...
if (!JcrNtLexicon.BASE.equals(nodeType.getInternalName())) {
// The node type is not 'nt:base', which
viewDefinition.append(" WHERE ");
int mixinTypeCount = 0;
int primaryTypeCount = 0;
StringBuilder mixinTypes = new StringBuilder();
StringBuilder primaryTypes = new StringBuilder();
Collection<JcrNodeType> typeAndSubtypes = subtypesByName.get(nodeType);
for (JcrNodeType thisOrSupertype : typeAndSubtypes) {
if (thisOrSupertype.isMixin()) {
if (mixinTypeCount > 0) mixinTypes.append(',');
assert prefixesByUris.containsKey(thisOrSupertype.getInternalName().getNamespaceUri());
String name = thisOrSupertype.getInternalName().getString(registry);
mixinTypes.append('[').append(name).append(']');
++mixinTypeCount;
} else {
if (primaryTypeCount > 0) primaryTypes.append(',');
assert prefixesByUris.containsKey(thisOrSupertype.getInternalName().getNamespaceUri());
String name = thisOrSupertype.getInternalName().getString(registry);
primaryTypes.append('[').append(name).append(']');
++primaryTypeCount;
}
}
if (primaryTypeCount > 0) {
viewDefinition.append('[').append(JcrLexicon.PRIMARY_TYPE.getString(registry)).append(']');
if (primaryTypeCount == 1) {
viewDefinition.append('=').append(primaryTypes);
} else {
viewDefinition.append(" IN (").append(primaryTypes).append(')');
}
}
if (mixinTypeCount > 0) {
if (primaryTypeCount > 0) viewDefinition.append(" OR ");
viewDefinition.append('[').append(JcrLexicon.MIXIN_TYPES.getString(registry)).append(']');
if (mixinTypeCount == 1) {
viewDefinition.append('=').append(mixinTypes);
} else {
viewDefinition.append(" IN (").append(mixinTypes).append(')');
}
}
}
// Define the view ...
builder.addView(tableName, viewDefinition.toString());
if (hasResidualProperties) {
// Record that there are residual properties ...
builder.markExtraColumns(tableName);
}
}
@Override
public Table getTable( SelectorName name ) {
return schemata.getTable(name);
}
/**
* Get a schemata instance that works with the supplied session and that uses the session-specific namespace mappings. Note
* that the resulting instance does not change as the session's namespace mappings are changed, so when that happens the
* JcrSession must call this method again to obtain a new schemata.
*
* @param session the session; may not be null
* @return the schemata that can be used for the session; never null
*/
public Schemata getSchemataForSession( JcrSession session ) {
assert session != null;
// If the session does not override any namespace mappings used in this schemata ...
if (!overridesNamespaceMappings(session)) {
// Then we can just use this schemata instance ...
return this;
}
// Otherwise, the session has some custom namespace mappings, so we need to return a session-specific instance...
return new SessionSchemata(session);
}
/**
* Determine if the session overrides any namespace mappings used by this schemata.
*
* @param session the session; may not be null
* @return true if the session overrides one or more namespace mappings used in this schemata, or false otherwise
*/
private boolean overridesNamespaceMappings( JcrSession session ) {
NamespaceRegistry registry = session.context().getNamespaceRegistry();
if (registry instanceof LocalNamespaceRegistry) {
Set<Namespace> localNamespaces = ((LocalNamespaceRegistry)registry).getLocalNamespaces();
if (localNamespaces.isEmpty()) {
// There are no local mappings ...
return false;
}
for (Namespace namespace : localNamespaces) {
if (prefixesByUris.containsKey(namespace.getNamespaceUri())) return true;
}
// None of the local namespace mappings overrode any namespaces used by this schemata ...
return false;
}
// We can't find the local mappings, so brute-force it ...
for (Namespace namespace : registry.getNamespaces()) {
String expectedPrefix = prefixesByUris.get(namespace.getNamespaceUri());
if (expectedPrefix == null) {
// This namespace is not used by this schemata ...
continue;
}
if (!namespace.getPrefix().equals(expectedPrefix)) return true;
}
return false;
}
@Override
public String toString() {
return schemata.toString();
}
/**
* Implementation class that builds the tables lazily.
*/
@NotThreadSafe
protected class SessionSchemata implements Schemata {
private final JcrSession session;
private final ExecutionContext context;
private final ImmutableSchemata.Builder builder;
private final NameFactory nameFactory;
private Schemata schemata;
protected SessionSchemata( JcrSession session ) {
this.session = session;
this.context = this.session.context();
this.nameFactory = context.getValueFactories().getNameFactory();
this.builder = ImmutableSchemata.createBuilder(context, session.nodeTypes());
// Add the "AllNodes" table ...
addAllNodesTable(builder, context, null, null);
this.schemata = builder.build();
}
@Override
public Table getTable( SelectorName name ) {
Table table = schemata.getTable(name);
if (table == null) {
// Try getting it ...
Name nodeTypeName = nameFactory.create(name.name());
JcrNodeType nodeType = getNodeType(nodeTypeName);
if (nodeType == null) return null;
addView(builder, context, nodeType);
schemata = builder.build();
}
return schemata.getTable(name);
}
}
}