package net.bitpot.railways.parser; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; import net.bitpot.railways.models.RailsEngine; import net.bitpot.railways.models.Route; import net.bitpot.railways.models.RouteList; import net.bitpot.railways.models.requestMethods.RequestMethod; import net.bitpot.railways.models.routes.EngineRoute; import net.bitpot.railways.models.routes.RedirectRoute; import net.bitpot.railways.models.routes.SimpleRoute; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.*; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Class parses text and retrieves RouteNode */ public class RailsRoutesParser extends AbstractRoutesParser { @SuppressWarnings("unused") private static Logger log = Logger.getInstance(RailsRoutesParser.class.getName()); // Errors public static final int NO_ERRORS = 0; public static final int ERROR_GENERAL = -1; public static final int ERROR_RAKE_TASK_NOT_FOUND = -2; private static final Pattern LINE_PATTERN = Pattern.compile("^([a-z0-9_]+)?\\s*([A-Z|]+)?\\s+(\\S+?)\\s+(.+?)$"); private static final Pattern ACTION_PATTERN = Pattern.compile(":action\\s*=>\\s*['\"](.+?)['\"]"); private static final Pattern CONTROLLER_PATTERN = Pattern.compile(":controller\\s*=>\\s*['\"](.+?)['\"]"); private static final Pattern REQUIREMENTS_PATTERN = Pattern.compile("(\\{.+?\\}\\s*$)"); private static final Pattern REDIRECT_PATTERN = Pattern.compile("redirect\\(\\d+(?:,\\s*(.+?))?\\)"); private static final String EXCEPTION_REGEX = "(?s)rake aborted!\\s*(.+?)Tasks:"; // Will capture both {:to => Test::Server} and Test::Server. private static final Pattern RACK_CONTROLLER_PATTERN = Pattern.compile("([A-Z_][A-Za-z0-9_:/]+)"); private static final Pattern HEADER_LINE = Pattern.compile("^\\s*Prefix\\s+Verb"); private static final Pattern ENGINE_ROUTES_HEADER_LINE = Pattern.compile("^Routes for ([a-zA-Z0-9:_]+):"); private String stacktrace; //private final Project project; private Module myModule; private int errorCode; private List<RailsEngine> mountedEngines; private RouteList routes; private int insertPos; @Nullable private RailsEngine currentEngine; public RailsRoutesParser() { this(null); } public RailsRoutesParser(@Nullable Module module) { myModule = module; clear(); clearErrors(); } private void clearErrors() { stacktrace = ""; errorCode = NO_ERRORS; } public RouteList parse(String stdOut, @Nullable String stdErr) { parseErrors(stdErr); return parse(new ByteArrayInputStream(stdOut.getBytes())); } @Override public RouteList parse(InputStream stream) { try { clear(); DataInputStream ds = new DataInputStream(stream); BufferedReader br = new BufferedReader(new InputStreamReader(ds)); String strLine; List<Route> routeList; //Read File Line By Line while ((strLine = br.readLine()) != null) { if (parseSpecialLine(strLine)) continue; routeList = parseLine(strLine); if (routeList != null) { addRoutes(routeList); addRakeEngineIfPresent(routeList); } } return routes; } catch (IOException e) { e.printStackTrace(); } return null; } private void addRoutes(List<Route> routeList) { if (insertPos < 0) routes.addAll(routeList); else { routes.addAll(insertPos, routeList); insertPos += routeList.size(); } } private void clear() { routes = new RouteList(); insertPos = -1; currentEngine = null; mountedEngines = new ArrayList<RailsEngine>(); } /** * Adds rake engine to the list of parsed engines. * @param routeList Route list parsed from a line. */ private void addRakeEngineIfPresent(@NotNull List<Route> routeList) { if (routeList.size() != 1) return; Route route = routeList.get(0); if (route instanceof EngineRoute) { mountedEngines.add(new RailsEngine( route.getQualifiedActionTitle(), route.getPath(), route.getRouteName())); } } /** * Parses special lines, such as Header, or line with information about * routes engine. Returns true if it matches special line pattern and was * successfully parsed, false otherwise. * * @param line Line from rake routes. * @return true if line is a special line and was parsed successfully. */ public boolean parseSpecialLine(String line) { Matcher matcher = HEADER_LINE.matcher(line); if (matcher.find()) return true; matcher = ENGINE_ROUTES_HEADER_LINE.matcher(line); if (matcher.find()) { // All following routes belong to parsed route engine. We should // find engine mount route and add them after it. String engineName = getGroup(matcher, 1); int index = findEngineRouteIndex(engineName); if (index >= 0) { insertPos = index + 1; currentEngine = findEngine(engineName); } return true; } return false; } @Nullable private RailsEngine findEngine(String engineName) { for(RailsEngine engine: mountedEngines) if (engine.getRubyClassName().equals(engineName)) return engine; return null; } private int findEngineRouteIndex(String engineName) { for(int i = 0; i < routes.size(); i++) if (routes.get(i).getQualifiedActionTitle().equals(engineName)) return i; return -1; } /** * Parses standard line from the output of rake 'routes' task. If this line contains route information, * new Route will be created and its fields set with appropriate parsed values. * * @param line Line from 'rake routes' output * @return Route object, if line contains route information, null if parsing failed. */ public List<Route> parseLine(String line) { // 1. Break line into 3 groups - [name]+[verb], path, conditions(action, controller) Matcher groups = LINE_PATTERN.matcher(line.trim()); if (groups.matches()) { String routeController = "", routeAction = ""; String routeName = getGroup(groups, 1); String routePath = getGroup(groups, 3); String conditions = getGroup(groups, 4); String[] actionInfo = conditions.split("#", 2); String engineClass = ""; String redirectPath = null; // null - when it's not redirect // Process new format of output: 'controller#action' if (actionInfo.length == 2) { routeController = actionInfo[0]; // In this case second part can contain additional requirements. Example: // "index {:user_agent => /something/}" routeAction = extractRouteRequirements(actionInfo[1]); } else { Matcher redirectMatcher = REDIRECT_PATTERN.matcher(conditions); if (redirectMatcher.matches()) redirectPath = getGroup(redirectMatcher, 1); else { // Older format - all route requirements are specified in ruby hash: // {:controller => 'users', :action => 'index'} routeController = captureGroup(CONTROLLER_PATTERN, conditions); routeAction = captureGroup(ACTION_PATTERN, conditions); // Check reference to mounted engine. if (routeController.isEmpty() && routeAction.isEmpty()) engineClass = captureGroup(RACK_CONTROLLER_PATTERN, conditions); // Else just set action to provided text. if (routeAction.isEmpty() && routeController.isEmpty() && engineClass.isEmpty()) routeAction = conditions; } } // We can have several request methods here: "GET|POST" String[] requestMethods = getGroup(groups, 2).split("\\|"); List<Route> result = new ArrayList<Route>(); // Also fix path if this route belongs to some engine if (currentEngine != null) { if (routePath.equals("/")) routePath = currentEngine.getRootPath(); else routePath = currentEngine.getRootPath() + routePath; } for (String requestMethodName : requestMethods) { Route route; if (!engineClass.isEmpty()) { route = new EngineRoute(myModule, RequestMethod.get(requestMethodName), routePath, routeName, engineClass); } else if (redirectPath != null) { route = new RedirectRoute(myModule, RequestMethod.get(requestMethodName), routePath, routeName, redirectPath); } else { route = new SimpleRoute(myModule, RequestMethod.get(requestMethodName), routePath, routeName, routeController, routeAction); } route.setParentEngine(currentEngine); result.add(route); } return result; } else { // TODO: string not matched. Should log this error somehow. } return null; } /** * Extracts requirements from second part and fills route information. * * @param actionWithReq Action with possible requirements part * @return Route action name without requirements. */ private String extractRouteRequirements(String actionWithReq) { String requirements = captureGroup(REQUIREMENTS_PATTERN, actionWithReq); // Return action without requirements return actionWithReq.substring(0, actionWithReq.length() - requirements.length()).trim(); } @NotNull private String getGroup(Matcher matcher, int groupNum) { String s = matcher.group(groupNum); return (s != null) ? s.trim() : ""; } /** * Captures first group in subject * * @param pattern Regex pattern * @param subject Subject string * @return Captured group or an empty string. */ private String captureGroup(Pattern pattern, String subject) { Matcher m = pattern.matcher(subject); if (m.find()) return m.group(1); return ""; } public void parseErrors(@Nullable String stdErr) { clearErrors(); if (stdErr == null) return; // Remove all rake messages that go to stdErr. Those messages start with "**". String cleanStdErr = stdErr.replaceAll("(?m)^\\*\\*.*$", "").trim(); if (cleanStdErr.equals("")) return; if (cleanStdErr.contains("Don't know how to")) errorCode = ERROR_RAKE_TASK_NOT_FOUND; else { errorCode = ERROR_GENERAL; // Remove unnecessary text if exception was thrown after rake sent several messages to stderr. stacktrace = cleanStdErr.replaceAll(EXCEPTION_REGEX, "$1"); } } public String getErrorStacktrace() { return stacktrace; } public boolean isErrorReported() { return errorCode != NO_ERRORS; } public int getErrorCode() { return errorCode; } public List<RailsEngine> getMountedEngines() { return mountedEngines; } }