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}