private static void unquotePropertyValues(
      Map<String, JSONValue> componentProperties, String componentType) {
    // From the component database, get the map of property names and types for the component type.
    Map<String, String> propertyTypesByName =
        COMPONENT_DATABASE.getPropertyTypesByName(componentType);

    // Iterate through the component properties.
    for (String propertyName : componentProperties.keySet()) {
      // Get the property type.
      String propertyType = propertyTypesByName.get(propertyName);
      // In theory the check for propertyType == null shouldn't be necessary
      // but I have sometimes had a problem with it being null when running
      // with GWT debugging. Maybe it changes the timing somehow. Anyway,
      // this test for null should not hurt anything. -Sharon
      if (propertyType == null) {
        OdeLog.wlog(
            "Couldn't find propertyType for property "
                + propertyName
                + " in component type "
                + componentType);
        continue;
      }
      // If the property type is one that was previously quoted, unquote the value.
      if (propertyType.equals("asset")
          || propertyType.equals("BluetoothClient")
          || propertyType.equals("component")
          || propertyType.equals("lego_nxt_sensor_port")
          || propertyType.equals("string")) {
        // Unquote the property value.
        JSONValue propertyValue = componentProperties.get(propertyName);
        String propertyValueString = propertyValue.asString().getString();
        propertyValueString = StringUtils.unquote(propertyValueString);
        componentProperties.put(propertyName, new ClientJsonString(propertyValueString));
      }
    }
  }
 /*
  * Format a description string as a json string. Note that the returned value
  * include surrounding double quotes.
  */
 private static String formatDescription(String description) {
   return StringUtils.toJson(description);
 }
  @VisibleForTesting
  static String processCompilerOutput(String output, String srcPath) {
    // First, remove references to the temp source directory from the messages.
    String messages = output.replace(srcPath, "");

    // Then, format warnings and errors nicely.
    try {
      // Split the messages by \n and process each line separately.
      String[] lines = messages.split("\n");
      Pattern pattern = Pattern.compile("(.*?):(\\d+):\\d+: (error|warning)?:? ?(.*?)");
      StringBuilder sb = new StringBuilder();
      boolean skippedErrorOrWarning = false;
      for (String line : lines) {
        Matcher matcher = pattern.matcher(line);
        if (matcher.matches()) {
          // Determine whether it is an error or warning.
          String kind;
          String spanClass;
          // Scanner messages do not contain either 'error' or 'warning'.
          // I treat them as errors because they prevent compilation.
          if ("warning".equals(matcher.group(3))) {
            kind = "WARNING";
            spanClass = "compiler-WarningMarker";
          } else {
            kind = "ERROR";
            spanClass = "compiler-ErrorMarker";
          }

          // Extract the filename, lineNumber, and message.
          String filename = matcher.group(1);
          String lineNumber = matcher.group(2);
          String text = matcher.group(4);

          // If the error/warning is in a yail file, generate a div and append it to the
          // StringBuilder.
          if (filename.endsWith(YoungAndroidConstants.YAIL_EXTENSION)) {
            skippedErrorOrWarning = false;
            sb.append(
                "<div><span class='"
                    + spanClass
                    + "'>"
                    + kind
                    + "</span>: "
                    + StringUtils.escape(filename)
                    + " line "
                    + lineNumber
                    + ": "
                    + StringUtils.escape(text)
                    + "</div>");
          } else {
            // The error/warning is in runtime.scm. Don't append it to the StringBuilder.
            skippedErrorOrWarning = true;
          }

          // Log the message, first truncating it if it is too long.
          if (text.length() > MAX_COMPILER_MESSAGE_LENGTH) {
            text = text.substring(0, MAX_COMPILER_MESSAGE_LENGTH);
          }
        } else {
          // The line isn't an error or a warning. This is expected.
          // If the line begins with two spaces, it is a continuation of the previous
          // error/warning.
          if (line.startsWith("  ")) {
            // If we didn't skip the most recent error/warning, append the line to our
            // StringBuilder.
            if (!skippedErrorOrWarning) {
              sb.append(StringUtils.escape(line)).append("<br>");
            }
          } else {
            skippedErrorOrWarning = false;
            // We just append the line to our StringBuilder.
            sb.append(StringUtils.escape(line)).append("<br>");
          }
        }
      }
      messages = sb.toString();
    } catch (Exception e) {
      // Report exceptions that happen during the processing of output, but don't make the
      // whole build fail.
      e.printStackTrace();

      // We were not able to process the output, so we just escape for HTML.
      messages = StringUtils.escape(messages);
    }

    return messages;
  }
  @Override
  public UserProject importProject(
      String userId,
      String projectName,
      InputStream uploadedFileStream,
      @Nullable String projectHistory)
      throws FileImporterException, IOException {
    // The projectName parameter has already been validated, including checking for an
    // existing project with the same name. (See TextValidators.checkNewProjectName).

    // Begin creating the project.
    Project project = new Project(projectName);
    project.setProjectType(YoungAndroidProjectNode.YOUNG_ANDROID_PROJECT_TYPE);

    // As we process the ZipEntry for each file, we'll adjust the directory structure so that it is
    // appropriate for this user.
    // Here we get the information (such as the qualified form name) that we'll need to do that.
    String qualifiedFormName =
        StringUtils.getQualifiedFormName(storageIo.getUser(userId).getUserEmail(), projectName);
    String srcDirectory = YoungAndroidProjectService.getSourceDirectory(qualifiedFormName);

    ZipInputStream zin = new ZipInputStream(uploadedFileStream);
    boolean isProjectArchive = false; // have we found at least one project properties file?
    try {
      // Extract files
      while (true) {
        ZipEntry entry;
        try {
          entry = zin.getNextEntry();
          if (entry == null) {
            break;
          }
        } catch (ZipException e) {
          // The uploaded file is not a valid zip file
          throw new FileImporterException(UploadResponse.Status.NOT_PROJECT_ARCHIVE);
        }

        if (!entry.isDirectory()) {
          String fileName = entry.getName();

          if (fileName.equals(YoungAndroidProjectService.PROJECT_PROPERTIES_FILE_NAME)) {
            // The content for the youngandroidproject/project.properties file must be regenerated
            // so that it contains the correct entries for "main" and "name", which are dependent on
            // the projectName and qualifiedFormName.
            String content =
                YoungAndroidProjectService.getProjectPropertiesFileContents(
                    projectName, qualifiedFormName, null, null, null, null);
            project.addTextFile(new TextFile(fileName, content));
            isProjectArchive = true;

          } else if (fileName.equals(FileExporter.REMIX_INFORMATION_FILE_PATH)
              || fileName.equals(StorageUtil.ANDROID_KEYSTORE_FILENAME)) {
            // If the remix information file is present, we ignore it. In the past, a remix
            // information file was saved in the zip when project source was downloaded and
            // retrieved from the zip when it was uploaded. However, we no longer do that because
            // we don't have a way to verify that the contents of the remix information file is
            // accurate during the upload.
            // If a keystore file is present we ignore that too for now, since
            // we don't have per-project keystores. The only way to get such a
            // source zip at the moment is using the admin functionality to
            // download another user's project source.
            continue;

          } else {

            if (fileName.startsWith(YoungAndroidProjectService.SRC_FOLDER)) {
              // For files within the src folder, we need to update the directory that we put files
              // in. Adjust the fileName so that it corresponds to this project's package.
              fileName = srcDirectory + '/' + StorageUtil.basename(fileName);
            }

            // Get the file content from the ZipEntry.
            ByteArrayOutputStream contentStream = new ByteArrayOutputStream();
            ByteStreams.copy(zin, contentStream);

            project.addRawFile(new RawFile(fileName, contentStream.toByteArray()));
          }
        }
      }
    } finally {
      zin.close();
    }

    if (!isProjectArchive) {
      // The uploaded file seems to be a valid zip file, but it doesn't contain the project
      // properties file.
      throw new FileImporterException(UploadResponse.Status.NOT_PROJECT_ARCHIVE);
    }

    // Set project history if provided
    if (projectHistory != null) {
      project.setProjectHistory(projectHistory);
    }
    String settings = YoungAndroidProjectService.getProjectSettings(null, null, null, null);
    long projectId = storageIo.createProject(userId, project, settings);
    return storageIo.getUserProject(userId, projectId);
  }