/******************************************************************************* * Copyright (c) 2013 Red Hat, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Neil Guzman - ruby implementation (B#350066) *******************************************************************************/ package org.eclipse.linuxtools.internal.rpmstubby.parser; import java.io.IOException; import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.CoreException; import org.eclipse.linuxtools.internal.rpmstubby.StubbyLog; /** * Class to parse a Ruby gemspec to grab specfile properties. */ public class RubyGemParser { private IFile file; private String gemVariable; private Map<String, List<String>> mSetupDefinitions; private Map<String, ArrayList<String>> mSetupDependencies; private static final String SETUP_START = "^gem[:\\.]{1,2}specification[:\\.]{1,2}new(\\s+)?do(\\s+)?\\|(\\s+)?(\\w+)(\\s+)?\\|"; private static final String STRING = "(?:\\\"([^\\\"]+)\\\"|'([^']+)'|(?:%q|%Q)(?:([^\\w])([^/3].+)[^\\w]))"; // (%q|%Q) "value" | 'value' | {value} private static final String LIST = "(?!%q|%Q)(?:%w|%W)?(?:([\\W])(.+)[\\W])"; // (%w|%W) [value, value2] | {"value", "value2"} private static final String STRING_LIST = "(?:\\\"([^\\\"]+)\\\"|'([^']+)')(?:[, ])??"; // "test", "test2" | 'test' 'test2' private static final String GENERIC_LIST = "(?:\\S+)(?:\\s+)??"; // test, test2 | test test2 private static final String REPLACE_ME = "(%REPLACE_ME)"; private String simpleDefineRx = "(\\s+)?(?!#)(?:\\b(%REPLACE_ME)\\b\\.(\\w+))(\\s+)?=(?!=)(\\s+)?(.*)"; // gem.variable = ... private String genericDefineRx = "(\\s+)?(?!#)(?:\\b(%REPLACE_ME)\\b\\.(\\w+))(\\s+)?(.*)"; // gem.variable... private String simpleFunctionRx = "(\\s+)?(?!#)(?:\\b(%REPLACE_ME)\\b\\.(\\w+))(\\s+)?(?:\\((.*)\\))(.*)?"; // gem.variable(...) /** * Initialize * * @param file * The gemspec file * @throws IOException Could not read from file * @throws CoreException File is not valid */ public RubyGemParser(IFile file) throws IOException, CoreException { mSetupDefinitions = new HashMap<>(); mSetupDependencies = new HashMap<>(); if (file.getContents().available() <= 0) { return; } this.file = file; gemVariable = ""; parse(); } /** * Parse the Ruby gemspec file */ public void parse() { List<String> rawSetupDefinitions = new ArrayList<>(); String line = ""; long startPos; long endPos; try (RandomAccessFile raf = new RandomAccessFile(file.getRawLocation() .makeAbsolute().toFile(), "r")) { startPos = findStart(raf); endPos = findEnd(raf, startPos); raf.seek(startPos); while ((line = raf.readLine()) != null && raf.getFilePointer() < endPos) { rawSetupDefinitions.add(line); } List<String> lSetupDefinitions = prepareOptions(rawSetupDefinitions); for (String str : lSetupDefinitions) { parseLine(str); } } catch (IOException e) { StubbyLog.logError(e); } } /** * Get the values taken from parsing the file as a list object * * @param key * The gemspec option to get the values for * @return The values of the gemspec option */ public List<String> getValueList(String key) { List<String> rc = new ArrayList<>(); if (mSetupDependencies.containsKey(key)) { if (!mSetupDependencies.get(key).isEmpty()) { for (String element : mSetupDependencies.get(key)) { rc.add(element); } } } else if (mSetupDefinitions.containsKey(key)) { if (!mSetupDefinitions.get(key).isEmpty()) { for (String element : mSetupDefinitions.get(key)) { rc.add(element); } } } return rc; } /** * Parses a line to figure out what type of line it is * * @param str * The line to parse */ private void parseLine(String str) { if (str.matches(simpleDefineRx)) { parseSimpleDefine(str); } else if (str.matches(simpleFunctionRx)) { parseSimpleFunction(str); } else if (str.matches(genericDefineRx)) { parseGenericOption(str); } } /** * Parses a string to figure its value * * @param str * The string parse * @return A list of objects that was found */ private static List<String> parseValue(String str) { List<String> rc = new ArrayList<>(); String temp = str.trim(); Pattern pattern = null; Matcher variableMatcher = null; if (temp.matches(STRING)) { pattern = Pattern.compile(STRING, Pattern.CASE_INSENSITIVE); variableMatcher = pattern.matcher(temp); // "" matches group 1 if (temp.startsWith("\"") && variableMatcher.matches()) { rc.add(variableMatcher.group(1)); } // '' matches group 2 else if (temp.startsWith("'") && variableMatcher.matches()) { rc.add(variableMatcher.group(2)); } // %q|%Q match else if ((temp.startsWith("%q") || temp.startsWith("%Q")) && variableMatcher.matches()) { rc.add(variableMatcher.group(4)); } } else if (temp.matches(LIST)) { pattern = Pattern.compile(LIST, Pattern.CASE_INSENSITIVE); variableMatcher = pattern.matcher(temp); if (variableMatcher.matches()) { rc.addAll(parseList(variableMatcher.group(2))); } } return rc; } /** * Parse the long string into a list * * @param str * The string to parse into a list * @return A list containing the found values */ private static List<String> parseList(String str) { List<String> rc = new ArrayList<>(); String temp = str.trim(); Pattern pattern = isPatternFoundList(str); if (pattern == null) { return rc; } Matcher variableMatcher = pattern.matcher(temp); if (variableMatcher != null) { while (variableMatcher.find()) { rc.add(variableMatcher.group()); } } return rc; } /** * Check if the string is a list of string values e.g. ["test", "test2"] OR * ['test' 'test2'] * * @param str * The string containing the list * @return The pattern of the string */ private static Pattern isPatternFoundList(String str) { Pattern rc = Pattern.compile(""); Pattern pattern = Pattern.compile(STRING_LIST, Pattern.CASE_INSENSITIVE); Matcher variableMatcher = pattern.matcher(str); if (variableMatcher.find()) { rc = pattern; } else { rc = isGenericFoundList(str); } return rc; } /** * Check if the string is a list of generic values (non-strings) e.g. [test, * test2] OR [test test2] * * @param str * The string containing the list * @return The pattern of the string */ private static Pattern isGenericFoundList(String str) { Pattern rc = Pattern.compile(""); Pattern pattern = Pattern.compile(GENERIC_LIST, Pattern.CASE_INSENSITIVE); Matcher variableMatcher = pattern.matcher(str); if (variableMatcher.find()) { rc = pattern; } return rc; } /** * Parse and grab the value of a simple define. The value taken is anything * after "gem.variable = ..." * * @param str * The simple define to parse */ private void parseSimpleDefine(String str) { String temp = str.trim(); Pattern pattern = Pattern.compile(simpleDefineRx, Pattern.CASE_INSENSITIVE); Matcher variableMatcher = pattern.matcher(temp); if (variableMatcher.find()) { String optionName = variableMatcher.group(2); List<String> value = parseValue(variableMatcher .group(5)); if (!value.isEmpty()) { mSetupDefinitions.put(optionName, value); } } } /** * Parse and grab the value of a simple function. The value taken is * anything in "gem.variable(...)" * * @param str * The function to parse */ private void parseSimpleFunction(String str) { String temp = str.trim(); Pattern pattern = Pattern.compile(simpleFunctionRx, Pattern.CASE_INSENSITIVE); Matcher variableMatcher = pattern.matcher(temp); if (variableMatcher.find()) { String functionName = variableMatcher.group(2); ArrayList<String> dependencies = new ArrayList<>(); dependencies.add(variableMatcher.group(4)); if (!mSetupDependencies.containsKey(functionName)) { mSetupDependencies.put(functionName, dependencies); } else { if (!mSetupDependencies.get(functionName).containsAll( dependencies)) { mSetupDependencies.get(functionName).addAll(dependencies); } } } } /** * Parse and grab the value of a generic option. The value taken is anything * after "gem.variable..." * * @param str * The option to parse */ private void parseGenericOption(String str) { String temp = str.trim(); Pattern pattern = Pattern.compile(genericDefineRx, Pattern.CASE_INSENSITIVE); Matcher variableMatcher = pattern.matcher(temp); if (variableMatcher.find()) { String functionName = variableMatcher.group(2); ArrayList<String> dependencies = new ArrayList<>(); dependencies.add(variableMatcher.group(4)); if (!mSetupDependencies.containsKey(functionName)) { mSetupDependencies.put(functionName, dependencies); } else { if (!mSetupDependencies.get(functionName).containsAll( dependencies)) { mSetupDependencies.get(functionName).addAll(dependencies); } } } } /** * Prepare the options within the specification by taking multiple lines and * concatenating them to the option it belongs to * * @param list * The list of options in their raw form * @return A refined list of options with a single line for each option */ private List<String> prepareOptions(List<String> list) { List<String> rc = new ArrayList<>(); String temp = ""; for (String str : list) { temp = str.trim(); if (isLineValidOption(temp)) { rc.add(str); } else if (!temp.startsWith("#") && !rc.isEmpty()) { rc.set(rc.size() - 1, rc.get(rc.size() - 1).concat(str)); } } return rc; } /** * Check to see if the line being read is a valid option within the * specification * * @param line * The line to check * @return True if the option within the specification is valid */ private boolean isLineValidOption(String line) { boolean rc = false; Pattern pattern = Pattern.compile(genericDefineRx, Pattern.CASE_INSENSITIVE); Matcher variableMatcher = pattern.matcher(line); if (variableMatcher.matches()) { rc = true; } return rc; } /** * Find the offset of when the specification starts and also store the * gemVariable used in the specification * * @param reader * The file reader * @return The position of the start of the specification * @throws IOException */ private long findStart(RandomAccessFile raf) throws IOException { long rc = -1; Pattern pattern = Pattern .compile( SETUP_START, Pattern.CASE_INSENSITIVE); Matcher variableMatcher = null; String line = ""; raf.seek(0); while ((line = raf.readLine()) != null && rc == -1) { variableMatcher = pattern.matcher(line.trim()); if (variableMatcher.matches()) { setGemVariable(variableMatcher.group(4)); rc = raf.getFilePointer(); } } return rc; } /** * Find the offset of when specification ends based on the position before * the end statement * * @param reader * The file reader * @param startPosition * The position of the start of the specification * @return The position of the end of the specification * @throws IOException */ private static long findEnd(RandomAccessFile raf, long startPos) throws IOException { long rc = -1; Pattern pattern = Pattern.compile("^end", Pattern.CASE_INSENSITIVE); Matcher variableMatcher = null; String line = ""; raf.seek(startPos); while ((line = raf.readLine()) != null && rc == -1) { variableMatcher = pattern.matcher(line.trim()); if (variableMatcher.matches()) { rc = raf.getFilePointer(); } } return rc; } /** * Sets the gemVariable taken from Gem::Specification.new do |gemVariable| * * @param str * The variable to set the gemVariable as */ private void setGemVariable(String str) { if (gemVariable.isEmpty()) { gemVariable = str.trim(); simpleDefineRx = simpleDefineRx.replace(REPLACE_ME, gemVariable); simpleFunctionRx = simpleFunctionRx.replace(REPLACE_ME, gemVariable); genericDefineRx = genericDefineRx.replace(REPLACE_ME, gemVariable); } } }