package io.sloeber.core.tools; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Status; import io.sloeber.core.common.Common; import io.sloeber.core.common.Const; /** * TxtFile is a class that hides the Arduino *.txt file processing <br/> * The is based on the code of Trump at * https://github.com/Trump211/ArduinoEclipsePlugin and later renamed from * Boards to TxtFile and adapted as needed. * * This class is at the root of processing the boards.txt platform.txt and * programmers.txt from the Arduino eco system As this feature is available most * other configuration stuff is put in files with the same setup and processed * by this class * * @author Jan Baeyens and trump * */ public class TxtFile { private File mLastLoadedTxtFile = null; private static final String DOT = Const.DOT; private static final String TXT_NAME_KEY_TAG = "name"; //$NON-NLS-1$ private static final String MENU = Const.MENU; private static final String MENUITEMID = "\\$\\{MENUITEMID}"; //$NON-NLS-1$ private static final String MENUID = "\\$\\{MENUID}"; //$NON-NLS-1$ private static final String BOARDID = "\\$\\{BOARDID}"; //$NON-NLS-1$ private static final String MENUITEMNAME = "\\$\\{MENUITEMNAME}";//$NON-NLS-1$ Map<String, String> settings = null; private LinkedHashMap<String, Map<String, String>> fileContent = new LinkedHashMap<>(); // all // the // data public TxtFile(File boardsFileName) { LoadBoardsFile(boardsFileName); } public TxtFile() { // no constructor needed } /** * This method returns the full section so custom processing can be done. * * @param SectionKey * the first name on the line before the . * @return all entries that match the filter */ public Map<String, String> getSection(String SectionKey) { return this.fileContent.get(SectionKey); } /** * Get all the menu option names in the .txt file * * @return a list of all the menu option names */ public Set<String> getMenuNames() { HashSet<String> ret = new HashSet<>(); for (Entry<String, Map<String, String>> entry : this.fileContent.entrySet()) { if (entry.getKey().equals(MENU)) { for (Entry<String, String> e2 : entry.getValue().entrySet()) { if (!e2.getKey().contains(Const.DOT)) { if (!ret.contains(e2.getValue())) { ret.add(e2.getValue()); } } } } } return ret; } /** * Get all the menu option names in the .txt file * * @return a list of all the menu option key value pairs */ public TreeMap<String, String> getMenus() { TreeMap<String, String> ret = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); for (Entry<String, Map<String, String>> entry : this.fileContent.entrySet()) { if (entry.getKey().equals(MENU)) { for (Entry<String, String> e2 : entry.getValue().entrySet()) { if (!e2.getKey().contains(Const.DOT)) { ret.put(e2.getKey().toUpperCase(), e2.getValue()); } } } } return ret; } /** * Get all the acceptable values for a option for a board The outcome of * this method can be used to fill a * * @param menu * the id of a menu not the name * @param boardID * the id of a board not the name * @return The nice names that are the possible selections */ public String[] getMenuItemNamesFromMenuID(String menuID, String boardID) { HashSet<String> ret = new HashSet<>(); Map<String, String> menuInfo = this.fileContent.get(MENU); if (menuInfo == null) { return new String[0]; } Map<String, String> boardInfo = this.fileContent.get(boardID); if (boardInfo != null) { String SearchKey = MENU + DOT + menuID + DOT; SearchKey = SearchKey.toUpperCase(); for (Entry<String, String> e2 : boardInfo.entrySet()) { int numsubkeys = e2.getKey().split("\\.").length; //$NON-NLS-1$ boolean startOk = e2.getKey().toUpperCase().startsWith(SearchKey); if ((numsubkeys == 3) && (startOk)) ret.add(e2.getValue()); } } return ret.toArray(new String[ret.size()]); } /** * Get all the acceptable values for a option for a board The outcome of * this method can be used to fill a * * @param menu * the id of a menu not the name * @param boardID * the id of a board not the name * @return The IDs that are the possible selections */ public String[] getMenuItemIDsFromMenuID(String menuID, String boardID) { HashSet<String> ret = new HashSet<>(); Map<String, String> menuInfo = this.fileContent.get(MENU); if (menuInfo == null) { return new String[0]; } Map<String, String> boardInfo = this.fileContent.get(boardID); if (boardInfo != null) { String SearchKey = MENU + DOT + menuID + DOT; SearchKey = SearchKey.toUpperCase(); for (Entry<String, String> e2 : boardInfo.entrySet()) { String[] subKeys = e2.getKey().split("\\.");//$NON-NLS-1$ int numsubkeys = subKeys.length; boolean startOk = e2.getKey().toUpperCase().startsWith(SearchKey); if ((numsubkeys == 3) && (startOk)) ret.add(subKeys[2]); } } return ret.toArray(new String[ret.size()]); } /** * this is public String[] getAllNames(String[] toaddNames) with a empty * toaddnames * * @return */ public String[] getAllNames() { return getAllNames(new String[0]); } /** * getAllNames returns all the "names" that are in the currently loaded * *.txt file. The toaddNames are added to the end result toaddNames should * be a string array and can not be null * * For a boards.txt file that means all the board names. For a * programmers.txt file that means all the programmers For platform.txt the * outcome is not defined * * @return an empty list if no board file is loaded. In all other cases it * returns the list of boards found in the file * @author Trump * */ public String[] getAllNames(String[] toaddNames) { if (this.mLastLoadedTxtFile.equals(new String())) { return toaddNames; } HashSet<String> allNames = new HashSet<>(); for (String curName : toaddNames) { allNames.add(curName); } for (String s : this.fileContent.keySet()) { if (s != null) { String theName = this.fileContent.get(s).get(TXT_NAME_KEY_TAG); if (theName != null) { allNames.add(theName); } } } String[] sBoards = new String[allNames.size()]; allNames.toArray(sBoards); Arrays.sort(sBoards); return sBoards; } /** * Load the board.txt file provided. * * @param BoardsFile * the full name to the boards.txt file * @return true when the action was successful. else false. * @author jan */ public boolean LoadBoardsFile(File boardsFile) { if ((this.mLastLoadedTxtFile != null) && (this.mLastLoadedTxtFile.equals(boardsFile))) return true; // do nothing when value didn't change this.mLastLoadedTxtFile = boardsFile; return LoadBoardsFile(); } /** * Load loads the board.txt file based on the arduino path. * * @param NewArduinoPath * the full path to the file board.txt (including board.txt) * @return true when the action was successful. else false. */ private boolean LoadBoardsFile() { // If the file doesn't exist ignore it. if (!this.mLastLoadedTxtFile.exists()) return false; this.fileContent.clear(); try { Map<String, String> boardPreferences = new LinkedHashMap<>(); load(this.mLastLoadedTxtFile, boardPreferences); for (Object k : boardPreferences.keySet()) { String key = (String) k; String board = key.substring(0, key.indexOf('.')); if (!this.fileContent.containsKey(board)) this.fileContent.put(board, new HashMap<String, String>()); (this.fileContent.get(board)).put(key.substring(key.indexOf('.') + 1), boardPreferences.get(key)); } } catch (Exception e) { Common.log(new Status(IStatus.WARNING, Const.CORE_PLUGIN_ID, Messages.Boards_Failed_to_read_boards + this.mLastLoadedTxtFile.getName(), e)); } return true; } /** * Given a nice name look for the ID The assumption is that the txt file * contains a line like ID.name=[nice name] Given this this method returns * ID when given [nice name] */ public String getBoardIDFromBoardName(String name) { if ((name == null) || name.isEmpty()) { return null; } for (Entry<String, Map<String, String>> entry : this.fileContent.entrySet()) { for (Entry<String, String> e2 : entry.getValue().entrySet()) { if (e2.getValue().equals(name)) return entry.getKey(); } } return null; } /** * Loads the input stream to a Map, ignoring any lines that start with a # * <p> * Taken from preferences.java in the arduino source * * @param input * the input stream to load * @param table * the Map to load the values to * @throws IOException * when something goes wrong?? */ static public void load(File inputFile, Map<String, String> table) throws IOException { try (FileInputStream input = new FileInputStream(inputFile);) { String[] lines = loadStrings(input); // Reads as UTF-8 for (String line : lines) { if ((line.length() == 0) || (line.charAt(0) == '#')) continue; // this won't properly handle = signs being in the text int equals = line.indexOf('='); if (equals != -1) { String key = line.substring(0, equals).trim(); String value = line.substring(equals + 1).trim(); table.put(key, value); } } input.close(); } } /** * Loads an input stream into an array of strings representing each line of * the input stream * * @param input * the input stream to load * @return the array of strings representing the inputStream */ static public String[] loadStrings(InputStream input) { try { String lines[] = new String[100]; int lineCount = 0; try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, "UTF-8"));) { //$NON-NLS-1$ String line = null; while ((line = reader.readLine()) != null) { if (lineCount == lines.length) { String temp[] = new String[lineCount << 1]; System.arraycopy(lines, 0, temp, 0, lineCount); lines = temp; } lines[lineCount++] = line; } reader.close(); } if (lineCount == lines.length) { return lines; } // resize array to appropriate amount for these lines String output[] = new String[lineCount]; System.arraycopy(lines, 0, output, 0, lineCount); return output; } catch (IOException e) { IStatus status = new Status(IStatus.WARNING, Const.CORE_PLUGIN_ID, "Failed to read stream ", e); //$NON-NLS-1$ Common.log(status); } return null; } /** * * @return the file name that is currently loaded */ public File getTxtFile() { return this.mLastLoadedTxtFile; } public String getMenuNameFromID(String menuID) { Map<String, String> menuSectionMap = getSection(MENU); if (menuSectionMap != null) { for (Entry<String, String> curOption : menuSectionMap.entrySet()) { if (curOption.getKey().equalsIgnoreCase(menuID)) { return curOption.getValue(); } } } return MENU + " ID " + menuID + Messages.Boards_not_found; //$NON-NLS-1$ } public String getMenuItemNameFromMenuItemID(String boardID, String menuID, String menuItemID) { // look in the pre 1.5.4 way "menu".menuid.boardid.menuitemid=name Map<String, String> menuSectionMap = getSection(MENU); String lookupValue = menuID + DOT + boardID + DOT + menuItemID; for (Entry<String, String> curOption : menuSectionMap.entrySet()) { if (curOption.getKey().equalsIgnoreCase(lookupValue)) return curOption.getValue(); } // nothing found so look in the post 1.5.4 way // boardid."menu".menuid.menuitemid=name Map<String, String> BoardIDSectionMap = getSection(boardID); lookupValue = MENU + DOT + menuID + DOT + menuItemID; for (Entry<String, String> curOption : BoardIDSectionMap.entrySet()) { if (curOption.getKey().equalsIgnoreCase(lookupValue)) return curOption.getValue(); } return Messages.Boards_Get_menu_item_name_from_id_did_not_find.replaceAll(MENUITEMID, menuItemID) .replaceAll(MENUID, menuID).replaceAll(BOARDID, boardID); } public String getNameFromID(String myBoardID) { Map<String, String> boardSection = getSection(myBoardID); if (boardSection == null) { return new String(); } return boardSection.get("name"); //$NON-NLS-1$ } /* * Returns the package name based on the boardsfile name Caters for the * packages (with version number and for the old way */ public String getPackage() { IPath platformFile = new Path(this.mLastLoadedTxtFile.toString().trim()); String architecture = platformFile.removeLastSegments(1).lastSegment(); if (architecture.contains(Const.DOT)) { // This is a version number so // package return platformFile.removeLastSegments(4).lastSegment(); } return platformFile.removeLastSegments(2).lastSegment(); } /* * Returns the architecture based on the platform file name Caters for the * packages (with version number and for the old way */ public String getArchitecture() { IPath platformFile = new Path(this.mLastLoadedTxtFile.toString().trim()); String architecture = platformFile.removeLastSegments(1).lastSegment(); if (architecture.contains(Const.DOT)) { // This is a version number so // package architecture = platformFile.removeLastSegments(2).lastSegment(); } return architecture; } public String getMenuIDFromMenuName(String menuName) { Map<String, String> menuSectionMap = getSection(MENU); if (menuSectionMap != null) { for (Entry<String, String> curOption : menuSectionMap.entrySet()) { if (curOption.getValue().equals(menuName)) { return curOption.getKey().toUpperCase(); } } } return MENU + Messages.Boards_name + menuName + Messages.Boards_not_found; } public String getMenuItemIDFromMenuItemName(String boardID, String menuID, String menuItemName) { // look in the pre 1.5.4 way "menu".menuid.boardid.menuitemid=name Map<String, String> menuSectionMap = getSection(MENU); String lookupValue = menuID + DOT + boardID + DOT; lookupValue = lookupValue.toUpperCase(); for (Entry<String, String> curOption : menuSectionMap.entrySet()) { if (curOption.getValue().equals(menuItemName)) { if (curOption.getKey().toUpperCase().startsWith(lookupValue)) return curOption.getKey().substring(lookupValue.length()).toUpperCase(); } } // nothing found so look in the post 1.5.4 way // boardid."menu".menuid.menuitemid=name Map<String, String> BoardIDSectionMap = getSection(boardID); lookupValue = MENU + DOT + menuID + DOT; lookupValue = lookupValue.toUpperCase(); for (Entry<String, String> curOption : BoardIDSectionMap.entrySet()) { if (curOption.getValue().equals(menuItemName)) { if (curOption.getKey().toUpperCase().startsWith(lookupValue)) return curOption.getKey().substring(lookupValue.length()).toUpperCase(); } } return Messages.getMenuItemIDFromMenuItemName.replaceAll(MENUITEMNAME, menuItemName).replaceAll(MENUID, menuID) .replaceAll(BOARDID, boardID); } }