001/* 002 * ModeShape (http://www.modeshape.org) 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.modeshape.sequencer.ddl; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.net.URL; 021import java.net.URLClassLoader; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.Collections; 025import java.util.HashMap; 026import java.util.LinkedList; 027import java.util.List; 028import java.util.Map; 029import java.util.Map.Entry; 030import java.util.Queue; 031import javax.jcr.Binary; 032import javax.jcr.NamespaceRegistry; 033import javax.jcr.Node; 034import javax.jcr.Property; 035import javax.jcr.RepositoryException; 036import javax.jcr.Value; 037import javax.jcr.ValueFactory; 038import org.modeshape.common.annotation.NotThreadSafe; 039import org.modeshape.common.logging.Logger; 040import org.modeshape.common.text.ParsingException; 041import org.modeshape.common.util.CheckArg; 042import org.modeshape.common.util.IoUtil; 043import org.modeshape.jcr.api.JcrConstants; 044import org.modeshape.jcr.api.Session; 045import org.modeshape.jcr.api.nodetype.NodeTypeManager; 046import org.modeshape.jcr.api.sequencer.Sequencer; 047import org.modeshape.sequencer.ddl.node.AstNode; 048 049/** 050 * A sequencer of DDL files. 051 */ 052@NotThreadSafe 053public class DdlSequencer extends Sequencer { 054 055 private static final Logger LOGGER = Logger.getLogger(DdlSequencer.class); 056 057 protected static final URL[] DEFAULT_CLASSPATH = new URL[] {}; 058 protected static final List<String> DEFAULT_GRAMMARS; 059 protected static final Map<String, DdlParser> STANDARD_PARSERS_BY_NAME; 060 061 static { 062 List<String> grammarNames = new ArrayList<String>(); 063 Map<String, DdlParser> parsersByName = new HashMap<String, DdlParser>(); 064 for (DdlParser parser : DdlParsers.BUILTIN_PARSERS) { 065 String grammarName = parser.getId().toLowerCase(); 066 grammarNames.add(grammarName); 067 parsersByName.put(grammarName, parser); 068 } 069 DEFAULT_GRAMMARS = Collections.unmodifiableList(grammarNames); 070 STANDARD_PARSERS_BY_NAME = Collections.unmodifiableMap(parsersByName); 071 } 072 073 private String[] parserGrammars = DEFAULT_GRAMMARS.toArray(new String[DEFAULT_GRAMMARS.size()]); 074 private URL[] classpath = DEFAULT_CLASSPATH; 075 private final Map<AstNode, Node> nodeMap = new HashMap<AstNode, Node>(); 076 077 /** 078 * Get the names of the grammars that should be considered during processing. The grammar names may be the case-insensitive 079 * {@link DdlParser#getId() identifier} of a built-in grammar, or the name of a {@link DdlParser} implementation class. 080 * 081 * @return the array of grammar names or classes; never null but possibly empty 082 */ 083 public String[] getGrammars() { 084 return parserGrammars; 085 } 086 087 /** 088 * Set the names of the grammars that should be considered during processing. The grammar names may be the case-insensitive 089 * {@link DdlParser#getId() identifier} of a built-in grammar, or the name of a {@link DdlParser} implementation class. 090 * 091 * @param grammarNamesOrClasses the names; may be null if the default grammar list should be used 092 */ 093 public void setGrammars( String[] grammarNamesOrClasses ) { 094 this.parserGrammars = grammarNamesOrClasses != null && grammarNamesOrClasses.length != 0 ? grammarNamesOrClasses : DEFAULT_GRAMMARS.toArray(new String[DEFAULT_GRAMMARS.size()]); 095 } 096 097 /** 098 * Get the names of the classloaders that should be used to load any non-standard DdlParser implementations specified in the 099 * list of grammars. 100 * 101 * @return the classloader names that make up the classpath; never null but possibly empty if the default classpath should be 102 * used 103 */ 104 public URL[] getClasspath() { 105 return classpath; 106 } 107 108 /** 109 * Set the names of the classloaders that should be used to load any non-standard DdlParser implementations specified in the 110 * list of grammars. 111 * 112 * @param classpath the classloader names that make up the classpath; may be null or empty if the default classpath should be 113 * used 114 */ 115 public void setClasspath( URL[] classpath ) { 116 this.classpath = classpath != null ? classpath : DEFAULT_CLASSPATH; 117 } 118 119 /** 120 * Method that creates the DdlParsers instance. This may be overridden in subclasses to creates specific implementations. 121 * 122 * @param parsers the list of DdlParser instances to use; may be empty or null 123 * @return the DdlParsers implementation; may not be null 124 */ 125 protected DdlParsers createParsers( List<DdlParser> parsers ) { 126 return new DdlParsers(parsers); 127 } 128 129 @SuppressWarnings( "unchecked" ) 130 protected List<DdlParser> getParserList() { 131 List<DdlParser> parserList = new LinkedList<DdlParser>(); 132 for (String grammar : getGrammars()) { 133 if (grammar == null) { 134 continue; 135 } 136 // Look for a standard parser using a case-insensitive name ... 137 String lowercaseGrammar = grammar.toLowerCase(); 138 DdlParser parser = STANDARD_PARSERS_BY_NAME.get(lowercaseGrammar); 139 if (parser == null) { 140 // Attempt to instantiate the parser if its a classname ... 141 try { 142 ClassLoader classloader = new URLClassLoader(getClasspath(), Thread.currentThread().getContextClassLoader()); 143 Class<DdlParser> componentClass = (Class<DdlParser>)Class.forName(grammar, true, classloader); 144 parser = componentClass.newInstance(); 145 } catch (Throwable e) { 146 if (classpath == null || classpath.length == 0) { 147 LOGGER.error(e, 148 DdlSequencerI18n.errorInstantiatingParserForGrammarUsingDefaultClasspath, 149 grammar, 150 e.getLocalizedMessage()); 151 } else { 152 LOGGER.error(e, 153 DdlSequencerI18n.errorInstantiatingParserForGrammarClasspath, 154 grammar, 155 classpath, 156 e.getLocalizedMessage()); 157 } 158 } 159 } 160 if (parser != null) { 161 parserList.add(parser); 162 } 163 } 164 return parserList; // okay if empty 165 } 166 167 @Override 168 public void initialize( NamespaceRegistry registry, 169 NodeTypeManager nodeTypeManager ) throws RepositoryException, IOException { 170 registerNodeTypes("StandardDdl.cnd", nodeTypeManager, true); 171 registerNodeTypes("dialect/derby/DerbyDdl.cnd", nodeTypeManager, true); 172 registerNodeTypes("dialect/oracle/OracleDdl.cnd", nodeTypeManager, true); 173 registerNodeTypes("dialect/postgres/PostgresDdl.cnd", nodeTypeManager, true); 174 } 175 176 @Override 177 public boolean execute( Property inputProperty, 178 Node outputNode, 179 Context context ) throws Exception { 180 Binary ddlContent = inputProperty.getBinary(); 181 CheckArg.isNotNull(ddlContent, "ddl content binary value"); 182 183 // make sure node map is empty 184 this.nodeMap.clear(); 185 186 // Look at the input path to get the name of the input node (or it's parent if it's "jcr:content") ... 187 String fileName = getNameOfDdlContent(inputProperty); 188 189 // Perform the parsing 190 final AstNode rootNode; 191 DdlParsers parsers = createParsers(getParserList()); 192 try (InputStream stream = ddlContent.getStream()) { 193 rootNode = parsers.parse(IoUtil.read(stream), fileName); 194 } catch (ParsingException e) { 195 LOGGER.error(e, DdlSequencerI18n.errorParsingDdlContent, e.getLocalizedMessage()); 196 return false; 197 } catch (IOException e) { 198 LOGGER.error(e, DdlSequencerI18n.errorSequencingDdlContent, e.getLocalizedMessage()); 199 return false; 200 } 201 202 Queue<AstNode> queue = new LinkedList<AstNode>(); 203 queue.add(rootNode); 204 while (queue.peek() != null) { 205 AstNode astNode = queue.poll(); 206 createFromAstNode(outputNode, astNode); 207 208 // Add the children to the queue ... 209 for (AstNode child : astNode.getChildren()) { 210 queue.add(child); 211 } 212 } 213 214 // second pass to lookup references (this allows for DDL to have forward references) 215 for (final Entry<AstNode, Node> entry : this.nodeMap.entrySet()) { 216 appendNodeProperties(entry.getKey(), entry.getValue()); 217 } 218 219 return true; 220 } 221 222 private void appendNodeProperties( AstNode astNode, 223 Node sequenceNode ) throws RepositoryException { 224 ValueFactory valueFactory = sequenceNode.getSession().getValueFactory(); 225 226 for (String propertyName : astNode.getPropertyNames()) { 227 Object astNodePropertyValue = astNode.getProperty(propertyName); 228 List<Value> valuesList = convertToPropertyValues(astNodePropertyValue, valueFactory); 229 if (valuesList.size() == 1) { 230 sequenceNode.setProperty(propertyName, valuesList.get(0)); 231 } else { 232 sequenceNode.setProperty(propertyName, valuesList.toArray(new Value[0])); 233 } 234 } 235 } 236 237 private Node createFromAstNode( Node parent, 238 AstNode astNode ) throws RepositoryException { 239 String relativePath = astNode.getAbsolutePath().substring(1); 240 Node sequenceNode = null; 241 242 // for SNS the absolute path will use first node it finds as the parent so find real parent if possible 243 Node parentNode = getNode(astNode.getParent()); 244 245 if (parentNode == null) { 246 sequenceNode = parent.addNode(relativePath, astNode.getPrimaryType()); 247 } else { 248 final Session session = (Session)parentNode.getSession(); 249 String jcrName = astNode.getName(); 250 251 // if first character is a '{' then the name is prefixed by the namespace URL 252 if ((jcrName.charAt(0) == '{') && (jcrName.indexOf('}') != -1)) { 253 final int index = jcrName.indexOf('}'); 254 String localName = jcrName.substring(index + 1); 255 localName = session.encode(localName); 256 257 jcrName = jcrName.substring(0, (index + 1)) + localName; 258 } else { 259 jcrName = session.encode(jcrName); 260 } 261 262 sequenceNode = parentNode.addNode(jcrName, astNode.getPrimaryType()); 263 } 264 265 this.nodeMap.put(astNode, sequenceNode); 266 for (String mixin : astNode.getMixins()) { 267 sequenceNode.addMixin(mixin); 268 } 269 astNode.removeProperty(JcrConstants.JCR_MIXIN_TYPES); 270 astNode.removeProperty(JcrConstants.JCR_PRIMARY_TYPE); 271 return sequenceNode; 272 } 273 274 private List<Value> convertToPropertyValues( Object objectValue, 275 ValueFactory valueFactory ) throws RepositoryException { 276 List<Value> result = new ArrayList<Value>(); 277 if (objectValue instanceof Collection) { 278 Collection<?> objects = (Collection<?>)objectValue; 279 for (Object childObjectValue : objects) { 280 List<Value> childValues = convertToPropertyValues(childObjectValue, valueFactory); 281 result.addAll(childValues); 282 } 283 } else if (objectValue instanceof Boolean) { 284 result.add(valueFactory.createValue((Boolean)objectValue)); 285 } else if (objectValue instanceof Integer) { 286 result.add(valueFactory.createValue((Integer)objectValue)); 287 } else if (objectValue instanceof Long) { 288 result.add(valueFactory.createValue((Long)objectValue)); 289 } else if (objectValue instanceof Double) { 290 result.add(valueFactory.createValue((Double)objectValue)); 291 } else if (objectValue instanceof Float) { 292 result.add(valueFactory.createValue((Float)objectValue)); 293 } else if (objectValue instanceof AstNode) { 294 result.add(valueFactory.createValue(getNode((AstNode)objectValue))); 295 } else { 296 result.add(valueFactory.createValue(objectValue.toString())); 297 } 298 return result; 299 } 300 301 private Node getNode( final AstNode node ) { 302 return this.nodeMap.get(node); 303 } 304 305 private String getNameOfDdlContent( Property inputProperty ) throws RepositoryException { 306 Node parentNode = inputProperty.getParent(); 307 if (JcrConstants.JCR_CONTENT.equalsIgnoreCase(parentNode.getName())) { 308 parentNode = parentNode.getParent(); 309 } 310 return parentNode.getName(); 311 } 312}