/* * To change this license header, choose License Headers in Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. */ package com.codeigniter.netbeans.shared; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Vector; import javax.swing.text.AbstractDocument; import javax.swing.text.Document; import org.netbeans.api.lexer.Token; import org.netbeans.api.lexer.TokenHierarchy; import org.netbeans.api.lexer.TokenSequence; import org.netbeans.modules.php.editor.lexer.PHPTokenId; /** * * @author dwoods */ public abstract class PHPDocumentParser { private static final String ciDocBase = "http://codeigniter.com/user_guide"; /** * Attempts to extract the class name and CI documentation URL from the file. * Note: Assumes there is at most 1 class per file. Will take the first class name encountered in the event of multiple classes * @param file * @return - CiClass for the file. The filename will be used as the className if no class is * declared in the file (E.g. a helper file) * @throws FileNotFoundException, IllegalArgumentException */ public static CiClass extractCiClass(File file) throws FileNotFoundException, IllegalArgumentException { if (!FileExtractor.isPHPFile(file)) { throw new IllegalArgumentException(String.format("%s is not a .php file\n", file.getAbsolutePath())); } CiClass retval = null; URL docLink = null; String className = null; TokenSequence<PHPTokenId> tokenSeq = getTokensFromFile(file); if (tokenSeq != null) { boolean done = false; while (!done && tokenSeq.moveNext()) { // Search for URL in the starting comment blocks Token token = tokenSeq.token(); if (token.id().equals(PHPTokenId.PHPDOC_COMMENT)) { String docComment = token.text().toString(); String[] arr = docComment.split("\\s+"); for (String s : arr) { if (s.startsWith(ciDocBase)) { try { // Update all links to point to the user_guide 3 s = s.replaceFirst("/user_guide/", "/userguide3/"); /* If http:// is used instead of http://www. then the code igniter website won't open at the specified function. For example: http://codeigniter.com/userguide3/libraries/table.html?#CI_Table.make_columns won't open at the location where make_columns is documented. It works if http://www. is used instead though */ s = s.replaceFirst("http://", "http://www."); docLink = new URL(s); } catch (MalformedURLException mue) { mue.printStackTrace(System.err); } // Done if the className has already been found, regardless of the success/failure of the URL done = (className != null); break; } } } else if (token.id().equals(PHPTokenId.PHP_CLASS)) { // Find the first string after the word "class" while (tokenSeq.moveNext()) { if (tokenSeq.token().id().equals(PHPTokenId.PHP_STRING)) { className = tokenSeq.token().text().toString(); done = (docLink != null); break; } } } } // If a class name couldn't be found, use the file name without the extension if (className == null) { int index = file.getName().lastIndexOf("."); className = file.getName().substring(0, index); } retval = new CiClass(className, docLink); } return retval; } /** * Extracts all public functions from the PHP file * @param file * @return ArrayList of CiFunctions * @throws FileNotFoundException * @throws IllegalArgumentException */ public static ArrayList<CiFunction> extractFunctions(File file) throws FileNotFoundException, IllegalArgumentException { if (!FileExtractor.isPHPFile(file)) { throw new IllegalArgumentException(String.format("%s is not a .php file\n", file.getAbsolutePath())); } ArrayList<CiFunction> retval = new ArrayList<CiFunction>(); TokenSequence<PHPTokenId> tokenSeq = getTokensFromFile(file); if (tokenSeq != null) { while (tokenSeq.moveNext()) { Token token = tokenSeq.token(); if (token.id().equals(PHPTokenId.PHP_FUNCTION)) { // Found a new function int backCount = 1; // Number of movePrevious() calls boolean isPublic = true; // Walk backwards to see if the function is private or protected while (tokenSeq.movePrevious()) { PHPTokenId id = tokenSeq.token().id(); if (!id.equals(PHPTokenId.WHITESPACE)) { if (id.equals(PHPTokenId.PHP_PROTECTED) || id.equals(PHPTokenId.PHP_PRIVATE)) { isPublic = false; } break; } backCount++; } // Move the token sequence back to pointing to the "function" keyword for (int i = 0; i < backCount; i++) { tokenSeq.moveNext(); } if (isPublic) { String funcName = null; // Get the function name while (tokenSeq.moveNext()) { if (tokenSeq.token().id().equals(PHPTokenId.PHP_STRING)) { funcName = tokenSeq.token().text().toString(); break; } } if (funcName != null) { // Create a string starting after the first "(" and ending before the closing ")" StringBuilder stringBuilder = new StringBuilder(); // Move tokenSeq to after the first "(" while (tokenSeq.moveNext()) { if (tokenSeq.token().text().equals("(")) { break; } } int count = 1; // Keep a count of unpaired brackets. When at 0 we've found the closing ")" while (tokenSeq.moveNext()) { if (tokenSeq.token().text().equals(")")) { count--; } else if (tokenSeq.token().text().equals("(")) { count++; } if (count == 0) { break; } else { stringBuilder = stringBuilder.append(tokenSeq.token().text()); } } Vector<CiParameter> params = parseParameterString(stringBuilder.toString()); CiFunction func = new CiFunction(funcName, params); retval.add(func); } } } } } return retval; } /** * Get the token at offset in the given Document * * @param doc document * @param offset offset in document * @return token */ public static Token<PHPTokenId> getToken(Document doc, int offset) { TokenSequence<PHPTokenId> ts = getTokenSequence(doc); if (ts == null) { return null; } ts.move(offset); ts.moveNext(); Token<PHPTokenId> token = ts.token(); return token; } /** * Get the TokenSequence for the full document * * @param doc document * @return tokens or null in the event of an error */ public static TokenSequence<PHPTokenId> getTokenSequence(Document doc) { AbstractDocument absDoc = (AbstractDocument) doc; absDoc.readLock(); TokenSequence<PHPTokenId> tokens = null; try { TokenHierarchy<Document> hierarchy = TokenHierarchy.get(doc); tokens = hierarchy.tokenSequence(PHPTokenId.language()); } finally { absDoc.readUnlock(); } return tokens; } /** * Gets the TokenSequence from the specified file * * @param file * @return TokenSequence for the file */ public static TokenSequence<PHPTokenId> getTokensFromFile(File file) throws FileNotFoundException { TokenSequence<PHPTokenId> retval = null; if (!FileExtractor.isPHPFile(file)) { throw new IllegalArgumentException( String.format("%s is not a PHP file\n", file.getPath())); } BufferedReader reader = new BufferedReader(new FileReader(file)); TokenHierarchy th = TokenHierarchy.create(reader, PHPTokenId.language(), null, null); retval = th.tokenSequence(); try { reader.close(); } catch (IOException ioe) { System.err.println("Unable to close reader in getTokensFromFile()\n"); } return retval; } /** * Extracts the parameters and their default values * The string should only contain the portion of the function between the parenthesis * For example, if the code is: public function foo($bar1, $bar2 = "somevalue") { * then the string should be "$bar1, $bar2 = "somevalue"" * @param str * @return */ private static Vector<CiParameter> parseParameterString(String str) { Vector<CiParameter> retval = new Vector<CiParameter>(); String[] params = str.split(","); for (String param : params) { String[] nameAndDefault = param.split("="); if (nameAndDefault.length == 1) { retval.add(new CiParameter(nameAndDefault[0].trim())); } else if (nameAndDefault.length == 2) { retval.add(new CiParameter(nameAndDefault[0].trim(), nameAndDefault[1].trim())); } else { assert(false); // Shouldn't ever have more than 2 items } } return retval; } }