/* * Licensed to Laurent Broudoux (the "Author") under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. Author licenses this * file to you 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 com.github.lbroudoux.dsl.eip.parser.camel; import java.io.File; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayDeque; import java.util.Deque; import java.util.HashMap; import java.util.List; import java.util.Map; import org.eclipse.emf.common.util.EList; import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTParser; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.Comment; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.LineComment; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.MethodInvocation; import com.github.lbroudoux.dsl.eip.Channel; import com.github.lbroudoux.dsl.eip.CompositeProcessor; import com.github.lbroudoux.dsl.eip.ConditionalRoute; import com.github.lbroudoux.dsl.eip.EIPModel; import com.github.lbroudoux.dsl.eip.EipFactory; import com.github.lbroudoux.dsl.eip.Endpoint; import com.github.lbroudoux.dsl.eip.Resequencer; import com.github.lbroudoux.dsl.eip.Route; import com.github.lbroudoux.dsl.eip.Router; /** * Parser for Apache Camel Java RouteBuilder class file. Just build a new instance and call * <code>parseAndFillModel()</code> with already initialized model and it should go ! * @author laurent */ public class CamelJavaFileParser extends ASTVisitor { private final File routeFile; private boolean inConfigure = false; private String routeSource = null; private CompilationUnit routeCU = null; // Because fluent APIs expression are parsed reversed (last invocation first), we // need a kind of Stack in order to easily recompose invocation order and corresponding // endpoints chain. private Deque<MethodInvocation> expressionStack = new ArrayDeque<>(); // Because comments are not globally accessibles, we need to recreate a map ordering // them bu line within source. private Map<Integer, String> commentMap = new HashMap<>(); /** The Route being parser and completed with endpoints and channels. */ private Route route = null; /** * Constructor. * @param routeFile The Java file representing Camel route configuration. */ public CamelJavaFileParser(File routeFile) { this.routeFile = routeFile; } /** * Parse the routeFile given while building the instance and fill the model. * @param model The EIP Model to fill with parsed elements from routeFile. * @throws InvalidArgumentException if given file is not a valid Spring integration file */ public void parseAndFillModel(EIPModel model) throws Exception { // Read source content. routeSource = parseRouteClass(); // Parse and get the compilation unit. ASTParser parser = ASTParser.newParser(AST.JLS8); parser.setKind(ASTParser.K_COMPILATION_UNIT); parser.setSource(routeSource.toCharArray()); parser.setResolveBindings(false); routeCU = (CompilationUnit) parser.createAST(null); // Visit and build a comment map before parsing. for (Object comment : routeCU.getCommentList()) { ((Comment) comment).accept(this); } // Initialize and build a route. route = EipFactory.eINSTANCE.createRoute(); model.getOwnedRoutes().add(route); // routeCU.accept(this); } // Override of ASTVisitor --------------------------------------------------- @Override public boolean visit(MethodDeclaration node) { if ("configure".equals(node.getName().toString())) { inConfigure = true; } return super.visit(node); } @Override public void endVisit(MethodDeclaration node) { if ("configure".equals(node.getName().toString())) { inConfigure = false; } super.endVisit(node); } @Override public boolean visit(MethodInvocation node) { if (inConfigure) { // We're in a fluent API usage... if (node.getExpression() != null) { expressionStack.addLast(node); } else { computeExpressionStack(node); // Reset stack for next expression. expressionStack.clear(); } } return super.visit(node); } @Override public boolean visit(LineComment node) { commentMap.put(routeCU.getLineNumber(node.getStartPosition()), getCommentContent(node)); return true; } // Private ------------------------------------------------------------------ /** */ private void computeExpressionStack(MethodInvocation first) { parseAndFillEndpoint(first, null, route.getOwnedEndpoints()); } /** */ private void parseAndFillEndpoint(MethodInvocation invocation, Channel incomingChannel, List<Endpoint> endpoints) { //System.err.println("Parsing " + invocation.getName()); Endpoint endpoint = null; if ("from".equals(invocation.getName().toString())) { // We may have some different stuffs here ! Check uri in order to guess... String uri = invocation.arguments().get(0).toString(); if (uri.startsWith("\"direct:")) { // That's a multicast subroute definition, use it to retrieve previously created // channel and place it as the current incomingChannel. int invocationLine = routeCU.getLineNumber(invocation.getStartPosition() + invocation.getLength()); String incomingChannelName = commentMap.get(invocationLine); incomingChannel = retrieveChannelByName(incomingChannelName, route.getOwnedChannels()); } else { endpoint = EipFactory.eINSTANCE.createGateway(); } } else if ("choice".equals(invocation.getName().toString())) { endpoint = EipFactory.eINSTANCE.createRouter(); } else if ("filter".equals(invocation.getName().toString())) { endpoint = EipFactory.eINSTANCE.createFilter(); } else if ("split".equals(invocation.getName().toString())) { // CompositeProcessor is implicit and should be created when split() appears. endpoint = EipFactory.eINSTANCE.createCompositeProcessor(); endpoints.add(endpoint); // Intermediate channel should be connected and then reset cause first // contained endpoint does not have incming channel. incomingChannel.setToEndpoint(endpoint); incomingChannel = null; // We should "go down" and consider composite endpoints until end() appear. endpoints = ((CompositeProcessor) endpoint).getOwnedEndpoints(); endpoint = EipFactory.eINSTANCE.createSplitter(); } else if ("when".equals(invocation.getName().toString())) { // Parent should be a Router. Endpoint lastEndpoint = endpoints.get(endpoints.size() - 1); if (lastEndpoint instanceof Router) { ConditionalRoute cRoute = EipFactory.eINSTANCE.createConditionalRoute(); ((Router) lastEndpoint).getOwnedRoutes().add(cRoute); // Inspect comment to get outgoing channel name. int invocationLine = routeCU.getLineNumber(invocation.getStartPosition() + invocation.getLength()); String outgoingChannelName = commentMap.get(invocationLine); // Outgoing channel will became net endpoint incoming. incomingChannel = EipFactory.eINSTANCE.createChannel(); incomingChannel.setName(outgoingChannelName); cRoute.setChannel(incomingChannel); } } else if ("otherwise".equals(invocation.getName().toString())) { // Everything should have been done at Router level... } else if ("end".equals(invocation.getName().toString())) { // We ended here a composite and should now "go up". Endpoint lastEndpoint = endpoints.get(endpoints.size() - 1); if (lastEndpoint.eContainer() instanceof CompositeProcessor) { endpoints = route.getOwnedEndpoints(); } } else if ("resequence".equals(invocation.getName().toString())) { endpoint = EipFactory.eINSTANCE.createResequencer(); } else if ("stream".equals(invocation.getName().toString())) { // Parent should be a Resequencer. Endpoint lastEndpoint = endpoints.get(endpoints.size() - 1); if (lastEndpoint instanceof Resequencer) { ((Resequencer) lastEndpoint).setStreamSequences(true); } } else if ("to".equals(invocation.getName().toString())) { // We may have a lot of stuffs here ! Check uri in order to guess... String uri = invocation.arguments().get(0).toString(); if (uri.startsWith("\"xslt:")) { endpoint = EipFactory.eINSTANCE.createTransformer(); } else if (uri.startsWith("\"switchyard:")) { endpoint = EipFactory.eINSTANCE.createServiceActivator(); } else if (uri.startsWith("\"direct:")) { // That's a multicast channel to a sub-route... int invocationLine = routeCU.getLineNumber(invocation.getStartPosition() + invocation.getLength()); String outgoingChannelName = commentMap.get(invocationLine); Channel multicast = retrieveChannelByName(outgoingChannelName, route.getOwnedChannels()); if (multicast == null) { multicast = EipFactory.eINSTANCE.createChannel(); multicast.setName(outgoingChannelName); route.getOwnedChannels().add(multicast); } Endpoint lastEndpoint = endpoints.get(endpoints.size() - 1); lastEndpoint.getToChannels().add(multicast); } } else { System.err.println("Got an unsupported: " + invocation.getName()); } if (endpoint != null) { // Complete Endpoint with common attributes if any and store it. int invocationLine = routeCU.getLineNumber(invocation.getStartPosition() + invocation.getLength()); String comment = commentMap.get(invocationLine); String endpointName = invocation.getName().toString() + "_" + endpoints.size(); String outgoingChannelName = null; // Comment may have "<endpoint_name>|<outgoing_channel_name>" or just "<endpoint_name>" format. if (comment != null) { if (comment.contains("|")) { endpointName = comment.substring(0, comment.indexOf('|')); outgoingChannelName = comment.substring(comment.indexOf('|') + 1); } else { endpointName = comment.trim(); } } endpoint.setName(endpointName); endpoints.add(endpoint); // Associate with incoming channel if any. if (incomingChannel != null) { incomingChannel.setToEndpoint(endpoint); } // We have created an endpoint so we need an outgoingChannel that // will become incoming one for next endpoint to create ! incomingChannel = EipFactory.eINSTANCE.createChannel(); if (outgoingChannelName != null) { incomingChannel.setName(outgoingChannelName); } incomingChannel.setFromEndpoint(endpoint); route.getOwnedChannels().add(incomingChannel); } if (!expressionStack.isEmpty()) { parseAndFillEndpoint(expressionStack.pollLast(), incomingChannel, endpoints); } } /** Browse the list of channels for retrieving the one having specified name. */ private Channel retrieveChannelByName(String name, EList<Channel> channels) { for (Channel channel : channels) { if (channel.getName().equals(name)) { return channel; } } return null; } /** Parse the Apache Camel route file and return content as String. */ private String parseRouteClass() throws Exception { return new String(Files.readAllBytes(Paths.get(routeFile.toURI()))); } /** Extract comment content from source. */ private String getCommentContent(Comment comment) { int start = comment.getStartPosition(); int end = start + comment.getLength(); String content = routeSource.substring(start, end); if (content.startsWith("//")) { content = content.substring(2).trim(); } return content; } }