/* * Copyright (C) 2012 The Android Open Source Project * * Licensed 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.motorola.studio.android.emulator.skin.android.parser; import static com.motorola.studio.android.common.log.StudioLogger.error; import static com.motorola.studio.android.common.log.StudioLogger.warn; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringReader; import java.net.URL; import java.nio.CharBuffer; import java.util.Collection; import java.util.EmptyStackException; import java.util.Stack; import com.motorola.studio.android.common.log.StudioLogger; import com.motorola.studio.android.emulator.EmulatorPlugin; import com.motorola.studio.android.emulator.core.exception.SkinException; import com.motorola.studio.android.emulator.i18n.EmulatorNLS; /** * DESCRIPTION: * This class parses a layout file into a LayoutFileModel object * * RESPONSIBILITY: * Parse the layout file * * COLABORATORS: * None. * * USAGE: * Call readLayout method passing a file to be parsed and retrieve the * correspondent LayoutFileModel object */ public class LayoutFileParser implements ILayoutConstants { /** * Name of the layout descriptor file at the skin folder */ private static final String LAYOUT_FILE_NAME = "layout"; /** * Name of the pseudo layout descriptor file at the res folder */ private static final String PSEUDO_LAYOUT_FILE = "res/pseudolayout"; /** * The pattern used for generating tokens out of the layout file */ private static final String SPLIT_PATTERN = "[\n\r \t]+"; /** * Parses a layout file * * @param skinFilesPath The path to the skin folder * * @return a model containing all data read from the layout file * * @throws SkinException If it is not possible to read the layout file */ public static LayoutFileModel readLayout(File skinFilesPath) throws SkinException { LayoutFileModel model = new LayoutFileModel(); File layoutPath = new File(skinFilesPath, LAYOUT_FILE_NAME); String fileContents; if ((layoutPath != null) && (layoutPath.isFile())) { fileContents = getLayoutFileContents(layoutPath); parseLayoutFile(fileContents, model); } Collection<String> partNames = model.getPartNames(); if ((model.getLayoutNames().size() == 0) && (partNames.size() == 1) && (partNames.iterator().next().equals(PartBean.UNIQUE_PART))) { fileContents = getPseudoLayoutFileContents(); parseLayoutFile(fileContents, model); } return model; } /** * Parses the provided layout file contents * * @param fileContents All the contents of a layout file * @param model The model where to set the parsed data * * @throws SkinException If the layout file is corrupted, or has erroneous syntax */ private static void parseLayoutFile(String fileContents, LayoutFileModel model) throws SkinException { // process given string to remove comments String cleanContents = ""; BufferedReader reader = null; try { StringBuffer contentBuffer = new StringBuffer(); reader = new BufferedReader(new StringReader(fileContents)); String line = null; do { line = reader.readLine(); String lineCopy = line; if ((line != null) && !lineCopy.trim().startsWith("#")) { contentBuffer.append(line + '\n'); } } while (line != null); cleanContents = contentBuffer.toString(); } catch (IOException e) { //try to continue with the parser cleanContents = fileContents; } finally { try { reader.close(); } catch (IOException e) { StudioLogger.error("Could not close input stream: ", e.getMessage()); //$NON-NLS-1$ } } String[] tokens = cleanContents.split(SPLIT_PATTERN); Stack<Object> stack = new Stack<Object>(); // At this point, the file has been read into hundreds of token, including blocks, // "{", "}", keys and values String currentTag = null; String key = null; // Iterate on the tokens try { for (String aToken : tokens) { // When the token is a "{", that means we need to stack something. This "something" // will be removed from stack when we find a matching "}" if (OPEN_BRACKET.equals(aToken)) { // Every word is interpreted as a key at first. If we find a "{", it must be // re-interpreted as a tag instead if (key != null) { currentTag = key; key = null; } addElementsToStack(stack, model, currentTag); } // When the token is a "}" we must remove something from the stack else if (CLOSE_BRACKET.equals(aToken)) { removeElementsFromStack(stack); } else { // A word is interpreted as a key by default. If the key is already set, we will // have a key-value pair and are able to assign it to something at the model if (key == null) { key = aToken; } else { setKeyValuePair(stack, model, currentTag, key, aToken); key = null; } } } } catch (EmptyStackException e) { throw new SkinException(EmulatorNLS.ERR_LayoutFileParser_BracketsDoNotMatch); } if (!stack.isEmpty()) { // When there is only a part bean at the first level, that means we have finished // parsing a single part layout. Remove it from the stack as well. // // NOTE: when creating the single part layout, we have added this additional element // to the stack if ((stack.size() == 1) && (stack.get(0) instanceof PartBean) && (((PartBean) stack.get(0)).getName().equals(PartBean.UNIQUE_PART))) { stack.pop(); } else { throw new SkinException(EmulatorNLS.ERR_LayoutFileParser_BracketsDoNotMatch); } } } /** * Reads the contents of the provided file into a String object * * @param layoutPath A file pointing to an "layout" file * * @return A string with all the contents of the file * * @throws SkinException If the file cannot be read */ private static String getLayoutFileContents(File layoutPath) throws SkinException { int fileSize = (int) layoutPath.length(); char[] buffer = new char[fileSize]; FileReader fr = null; try { fr = new FileReader(layoutPath); fr.read(buffer); } catch (IOException e) { error("The file " + layoutPath.getAbsolutePath() + " could not be read. cause=" + e.getMessage()); throw new SkinException(EmulatorNLS.ERR_LayoutFileParser_LayoutFileCouldNotBeRead); } finally { try { if (fr != null) { fr.close(); } } catch (IOException e) { warn("The file " + layoutPath.getAbsolutePath() + " could not be closed after reading"); } } return String.copyValueOf(buffer); } /** * Gets the contents of the pseudo layout file, for merging to the current model * * @return A string containing all the contents of the pseudo layout file * * @throws SkinException If the file cannot be read */ private static String getPseudoLayoutFileContents() throws SkinException { URL url = EmulatorPlugin.getDefault().getBundle().getResource(PSEUDO_LAYOUT_FILE); CharBuffer buffer = CharBuffer.allocate(1024); int readChars = 0; InputStream is = null; InputStreamReader isr = null; try { is = url.openStream(); isr = new InputStreamReader(is); while (readChars != -1) { readChars = isr.read(buffer); } buffer.flip(); } catch (IOException e) { error("The file res/pseudolayout could not be read. cause=" + e.getMessage()); throw new SkinException(EmulatorNLS.ERR_LayoutFileParser_LayoutFileCouldNotBeRead); } finally { try { if (is != null) { is.close(); } if (isr != null) { isr.close(); } } catch (IOException e) { warn("The file " + PSEUDO_LAYOUT_FILE + " could not be closed after reading"); } } return buffer.toString(); } /** * Stacks an element * The stack rules are quite complex. We first start by special cases (in which we analyze the stack * and the current element for accurate interpretation) and then move to the default cases. * * Summarizing, the stack will contain objects from this package (*Bean) as well as String objects. * When a bean is at the top of the stack, we may perform actions on the given object. Strings are added * to the stack for bracket matching and to add a mark for future actions. * * @param stack The stack where to add elements * @param model The model being built * @param elementName The name of the element to add to stack. */ private static void addElementsToStack(Stack<Object> stack, LayoutFileModel model, String elementName) { int stackSizeAtStart = stack.size(); //-------------- // SPECIAL CASES //-------------- // When the stack size is equal to zero, we can have one of those two situations: // // a) THE LAYOUT FILE CONTAINS MULTIPLE LAYOUT AND/OR PARTS: It is possible to have the // following tags: "parts", "layouts", "keyboard" or "network". All of them are handled in the // else clause, by adding the tag name at the stack // // b) THE LAYOUT FILE IS SIMPLE (i.e. it doesn't contain layouts, neither a collection // of parts): It is possible to have the following tags: "display", "background", "button", // "keyboard", "network". The first three belong to a part definition, so we need to include a // PartBean to the stack before the object representing the tag. The last two can be handled the same // way as in item (a) if (stack.size() == 0) { if ((MAIN_LEVEL_DISPLAY.equals(elementName)) || (MAIN_LEVEL_BACKGROUND.equals(elementName)) || (MAIN_LEVEL_BUTTON.equals(elementName))) { // This is a single part layout. Execute operation described at item (b) above PartBean bean = model.newPart(); stack.push(bean); if ((MAIN_LEVEL_BACKGROUND.equals(elementName)) || (MAIN_LEVEL_BUTTON.equals(elementName))) { stack.push(elementName); } else if (MAIN_LEVEL_DISPLAY.equals(elementName)) { RectangleBean display = bean.newDisplay(); stack.push(display); } } else { // PARTS, LAYOUTS, KEYBOARD, NETWORK stack.push(elementName); } } // When the stack size is equal to one, we can have one of those four situations: // // a) THE ELEMENT AT STACK IS NOT A STRING: In this case, we will handle as default case // b) THE ELEMENT AT STACK IS THE "parts" STRING: It means that the element name denotes the name of // a part. We must create a part with the name of the element, and add it to the stack // c) THE ELEMENT AT STACK IS THE "layouts" STRING: It means that the element name denotes the name of // a layout. We must create a layout with the name of the element, and add it to the stack // d) THE ELEMENT AT STACK IS ANY OTHER STRING: In this case, we will handle as default case else if (stack.size() == 1) { Object previousElement = stack.peek(); if (previousElement instanceof String) { if (MAIN_LEVEL_PARTS.equals((String) previousElement)) { // elementName is the name of a new part PartBean bean = model.newPart(elementName); stack.push(bean); } else if (MAIN_LEVEL_LAYOUTS.equals((String) previousElement)) { // elementName is the name of a new layout LayoutBean bean = model.newLayout(elementName); stack.push(bean); } } } //-------------- // DEFAULT CASES //-------------- // Any other case will be handled below. The following clauses cover any other remaining cases not // covered by the special cases. The beans created, when added to the stack, represents structures // already known. If it is not possible to guess what structure we need at the current parse iteration // or if we need an element at the stack to match a close bracket to come, we simply add it as string // // We only execute the following block if the previous cases didn't affect the stack if (stackSizeAtStart == stack.size()) { Object stackElem = stack.peek(); if (stackElem instanceof PartBean) { if (MAIN_LEVEL_DISPLAY.equals(elementName)) { RectangleBean display = ((PartBean) stackElem).newDisplay(); stack.push(display); } else if (MAIN_LEVEL_BACKGROUND.equals(elementName)) { ImagePositionBean background = ((PartBean) stackElem).newBackground(elementName); stack.push(background); } else { stack.push(elementName); } } else if (stackElem instanceof LayoutBean) { PartRefBean bean = ((LayoutBean) stackElem).newPartRef(elementName); stack.push(bean); } else if (stackElem instanceof String) { if ((MAIN_LEVEL_BUTTON.equals((String) stackElem) || (PART_BUTTONS .equals((String) stackElem)))) { Object nonStringObj = findFirstNonStringAtStack(stack); if (nonStringObj != null) { PartBean bean = (PartBean) nonStringObj; ImagePositionBean button = bean.newButton(elementName); stack.push(button); } } } } } /** * Removes an element from the stack * * @param stack The stack from where to remove elements */ private static void removeElementsFromStack(Stack<Object> stack) { stack.pop(); } /** * Set a key-value pair at the object at the top of the stack. * Depending on the key-value pair, we may set attributes to the model itself * * @param stack The stack containing the element to have a property set * @param model The model that can have a property set * @param currentTag The name of the tag containing a model property * @param key The property key * @param value The property value */ private static void setKeyValuePair(Stack<Object> stack, LayoutFileModel model, String currentTag, String key, String value) { Object obj = stack.peek(); if (obj instanceof String) { Object notStringObj = findFirstNonStringAtStack(stack); if (notStringObj instanceof ILayoutBean) { ((ILayoutBean) notStringObj).setKeyValue(key, value); } else { if (MAIN_LEVEL_NETWORK.equals(currentTag)) { if (NETWORK_DELAY.equals(key)) { model.setNetworkDelay(value); } else if (NETWORK_SPEED.equals(key)) { model.setNetworkSpeed(value); } } else if (MAIN_LEVEL_KEYBOARD.equals(currentTag)) { if (KEYBOARD_CHARMAP.equals(key)) { model.setKeyboardCharmap(value); } } } } else { ((ILayoutBean) obj).setKeyValue(key, value); } } /** * Utility method for finding the first non-String object at the stack * * @param stack The stack were to find the first non-String at * * @return The non-String object */ private static Object findFirstNonStringAtStack(Stack<Object> stack) { Object firstNonString = null; Object tmpObj = null; int i = stack.size() - 1; while (i >= 0) { tmpObj = stack.get(i); if (!(tmpObj instanceof String)) { firstNonString = tmpObj; break; } else { i--; } } return firstNonString; } }