Exemplo n.º 1
0
  /** Cancels the language selection. */
  private void cancel() {
    dispose();
    // Cancel was pressed, revert to the default language
    Language.loadAndActivateLanguage(Settings.DEFAULT_APP_LANGUAGE);
    Settings.remove(Settings.KEY_SETTINGS_VOICE);

    synchronized (WelcomeFrame.this) {
      Sounds.playSoundSample(Sounds.SAMPLE_THANK_YOU, false);
      WelcomeFrame.this.notify(); // Notify the main thread to continue starting the application
    }
  }
Exemplo n.º 2
0
  static {
    int TEXT_KEY_COLUMN_ = 0;
    int ORIGINAL_TEXT_COLUMN_ = 0;
    int TRANSLATION_COLUMN_ = 0;

    for (int i = 0; i < TEXTS_HEADER_KEYS.length; i++) {
      final String headerKey = TEXTS_HEADER_KEYS[i];
      TEXTS_HEADER_NAME_VECTOR.addElement(Language.getText(headerKey));
      if ("translationTool.tab.texts.table.header.textKeyContext".equals(headerKey))
        TEXT_KEY_COLUMN_ = i;
      else if ("translationTool.tab.texts.table.header.originalText".equals(headerKey))
        ORIGINAL_TEXT_COLUMN_ = i;
      else if ("translationTool.tab.texts.table.header.translation".equals(headerKey))
        TRANSLATION_COLUMN_ = i;
    }

    TEXT_KEY_COLUMN = TEXT_KEY_COLUMN_;
    ORIGINAL_TEXT_COLUMN = ORIGINAL_TEXT_COLUMN_;
    TRANSLATION_COLUMN = TRANSLATION_COLUMN_;
  }
Exemplo n.º 3
0
  /** Gets current text values and reassigns them to the GUI components. */
  private void reassignTexts() {
    setTitle(Language.getText("welcome.welcome") + " - " + Consts.APPLICATION_NAME);

    welcomeLabel.setText(Language.getText("welcome.welcome") + '!');
    firstRunLabel.setText(Language.getText("welcome.firstRun", Consts.APPLICATION_NAME + "™"));
    chooseLabel.setText(Language.getText("welcome.selectLanguage"));
    thankYouLabel.setText(Language.getText("welcome.thankYou", Consts.APPLICATION_NAME + "™"));
    languageLabel.setText(Language.getText("welcome.language"));
    voiceLabel.setText(Language.getText("welcome.voice"));

    languageLabel.setPreferredSize(null);
    voiceLabel.setPreferredSize(null);
    final int maxWidth =
        Math.max(languageLabel.getPreferredSize().width, voiceLabel.getPreferredSize().width);
    languageLabel.setPreferredSize(
        new Dimension(maxWidth, languageLabel.getPreferredSize().height));
    voiceLabel.setPreferredSize(new Dimension(maxWidth, voiceLabel.getPreferredSize().height));

    GuiUtils.updateButtonText(okButton, "button.ok");
    GuiUtils.updateButtonText(cancelButton, "button.cancel");

    pack();
    SharedUtils.centerWindow(this);
  }
Exemplo n.º 4
0
  /**
   * The entry point of the program.
   *
   * <p>Checks for running instances, and passes arguments if have to. Else loads the settings,
   * language files etc. and then instantiates the main frame.
   *
   * @param arguments if command line mode is desired, they will be handled by the {@link
   *     CliHandler}; else they will be treated as files (like replays, replay lists, replay
   *     sources) and will be opened properly
   * @throws MalformedURLException
   */
  public static void main(final String[] arguments) {
    // Add Sc2gears version and OS info to the User-Agent HTTP request property.
    // The final user agent string will be the value of this property + the default (which is the
    // Java version).
    System.setProperty(
        "http.agent",
        Consts.APPLICATION_NAME
            + "/"
            + Consts.APPLICATION_VERSION
            + " ("
            + System.getProperty("os.name")
            + "; "
            + System.getProperty("os.version")
            + "; "
            + System.getProperty("os.arch")
            + ")");

    checkFolders();

    Settings.loadProperties();

    final boolean cliMode = CliHandler.checkCliMode(arguments);

    if (!cliMode) {
      InstanceMonitor.checkRunningInstance(arguments);
      Log.init();
    }

    if (!cliMode) {
      installExtraLAFs();
      GuiUtils.setLAF(Settings.getString(Settings.KEY_SETTINGS_LAF));
    }

    if (Settings.doesSettingsFileExist()) {
      Language.loadAndActivateLanguage(Settings.getString(Settings.KEY_SETTINGS_LANGUAGE));
    } else {
      // Only show the welcome frame if we're not in CLI mode
      if (!cliMode) {
        final WelcomeFrame welcomeFrame = new WelcomeFrame();
        synchronized (welcomeFrame) {
          try {
            welcomeFrame.wait(); // Wait until the welcome frame is closed.
          } catch (final InterruptedException ie) {
            // This should never happen.
          }
        }
      }
    }

    // Now language is loaded, initialize codes that build on it.
    Language.applyDateTimeFormats();
    Settings.completeDefaultPropertiesInitialization();
    GuiUtils.initFileFilters();

    // Must be after completeDefaultPropertiesInitialization()!
    checkAndPerformPostUpdate();

    // Ensure ReplayUtils and AbilityCodesRepository is initialized
    // (else opening a replay from the file menu or by the CilHandler would fail!)
    try {
      Class.forName(ReplayUtils.class.getName());
    } catch (final ClassNotFoundException cfe) {
      // Never to happen
      cfe.printStackTrace();
    }

    if (cliMode) System.exit(CliHandler.handleArguments(arguments));
    else {
      // Load the Native class now, else later it might hang (dead lock?)
      if (GeneralUtils.isWindows()) // Currently JNA is only used on windows
      try {
          Class.forName(Native.class.getName());
        } catch (final ClassNotFoundException cfe) {
          // Never to happen
          cfe.printStackTrace();
        }

      // Apply proxy config
      applyProxyConfig();

      // Load plugins
      PluginManager.loadPlugins();

      // Now instantiate the main frame
      new MainFrame(arguments);
    }
  }
Exemplo n.º 5
0
  /** Creates a new TranslationToolDialog. */
  public TranslationToolDialog() {
    super("translationTool.title", Icons.LOCALE);

    final JPanel northPanel = new JPanel(new BorderLayout());
    northPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 0, 10));

    final Box languageChooserBox = Box.createVerticalBox();
    languageChooserBox.setBorder(BorderFactory.createEmptyBorder(10, 0, 15, 0));
    Box row = Box.createHorizontalBox();
    row.add(new JLabel(Language.getText("translationTool.chooseLanguageToEdit")));
    final JComboBox<String> editedLanguageComboBox = new JComboBox<>();
    final String EMPTY_LANGUAGE = " ";
    final Runnable rebuildEditedLanguageComboBoxTask =
        new Runnable() {
          @Override
          public void run() {
            editedLanguageComboBox.removeAllItems();
            editedLanguageComboBox.addItem(EMPTY_LANGUAGE);

            for (final String language : Language.getAvailableLanguages())
              if (!Settings.DEFAULT_APP_LANGUAGE.equals(language))
                editedLanguageComboBox.addItem(language);

            editedLanguageComboBox.setMaximumRowCount(editedLanguageComboBox.getItemCount());
          }
        };
    rebuildEditedLanguageComboBoxTask.run();
    editedLanguageComboBox.setSelectedIndex(0);
    editedLanguageComboBox.setRenderer(
        new BaseLabelListCellRenderer<String>(2) {
          @Override
          public Icon getIcon(final String value) {
            return EMPTY_LANGUAGE.equals(value) ? null : Icons.getLanguageIcon(value);
          }
        });
    row.add(editedLanguageComboBox);
    languageChooserBox.add(row);
    languageChooserBox.add(Box.createVerticalStrut(7));
    row = Box.createHorizontalBox();
    row.add(new JLabel(Language.getText("translationTool.orCreateNewTranslationWithName")));
    final JTextField newLanguageNameTextField = new JTextField(10);
    row.add(newLanguageNameTextField);
    final JButton createNewLanguageButton = new JButton();
    GuiUtils.updateButtonText(createNewLanguageButton, "translationTool.createButton");
    row.add(createNewLanguageButton);
    languageChooserBox.add(row);
    GuiUtils.alignBox(languageChooserBox, 1);

    languageChooserBox.add(Box.createVerticalStrut(20));
    final JPanel buttonsPanel = new JPanel(new GridLayout(1, 2));
    final JButton saveButton = new JButton(Icons.DISK);
    GuiUtils.updateButtonText(saveButton, "translationTool.saveChangesButton");
    buttonsPanel.add(saveButton);
    final JButton closeButton = createCloseButton("button.close");
    buttonsPanel.add(closeButton);
    languageChooserBox.add(buttonsPanel);

    northPanel.add(GuiUtils.wrapInPanel(languageChooserBox), BorderLayout.WEST);

    final JEditorPane notesEditorPane = new JEditorPane();
    notesEditorPane.setContentType("text/html");
    notesEditorPane.setText(
        "<html>"
            + "<b>NOTES</b><br><br>"
            + "With this Translation tool you can translate "
            + Consts.APPLICATION_NAME
            + " into other languages or edit any existing translations. If you create a new translation or update an existing one, please send the language file to me via email so I can include it in the next release.<br>"
            + "Each translation is stored in its own file. Language files are saved in the <i>\""
            + Consts.FOLDER_LANGUAGES
            + "\"</i> folder inside "
            + Consts.APPLICATION_NAME
            + ".<br>"
            + "Official language files appear in the application with a country flag. If you send me language files that I approve, I will add and associate the proper country flag for the language.<br>"
            + "Not all texts have to be translated, but of course the more the better. If a language file is incomplete, the original English version will be displayed for the missing texts.<br>"
            + "<br><i>Warning! If you update "
            + Consts.APPLICATION_NAME
            + ", the Updater will overwrite existing language files! Be sure to keep a copy of the language file you edit!</i><br><br><hr>"
            + "<b>PARAMETERS</b><br><br>"
            + "The values of texts may contain parameters which will be substituted by values specified by the program.<br>"
            + "The places and order of parameters are indicated with <span style='color:green'>$x</span> where <span style='color:green'>x</span> is the number identifier of the parameter (starting from 0).<br>"
            + "For example the following text: <span style='color:green'>Hello $1, this is a $0 day! You have $2$3.</span><br>"
            + "with the parameters: <span style='color:green'>\"fine\", \"Mr. Hunter\", '$', 10</span><br>"
            + "will result in: <span style='color:green'>Hello Mr. Hunter, this is a fine day! You have $10.<span style='color:green'><br><br><hr>"
            + "<b>HOTKEYS / MNEMONICS</b><br><br>"
            + "Menus and buttons may have hotkeys / mnemonics. The hotkey character can be marked with an _ (underscore) sign before the intended character.<br>"
            + "The hotkey indicators will be removed when displayed to the user.<br>"
            + "The hotkeys are optional. For example if we want a <span style='color:green'>\"Visit home page\"</span> button to have the ALT+M hotkey, the following text is to be specified: <span style='color:green'>\"Visit ho_me page\"</span><br><br><hr>"
            + "<b>ICONS</b><br><br>"
            + "Some texts appear with icons in the texts table. The following icons are defined:<ul>"
            + "<li><img border=0 src=\""
            + Icons.HTML.resource
            + "\">&nbsp;This icon indicates that the text is specified as HTML text. This is the case if the text starts with <i>\"&lt;html&gt;\"</i>; and in this case it has to end with <i>\"&lt;/html&gt;\"</i>."
            + "<li><img border=0 src=\""
            + Icons.KEYBOARD.resource
            + "\">&nbsp;This icon indicates that the text contains a hotkey / mnemonic marker."
            + "<li><img border=0 src=\""
            + Icons.DOCUMENT_ATTRIBUTE_P.resource
            + "\">&nbsp;This icon indicates that the text contains parameters."
            + "</ul>"
            + "</html>");
    notesEditorPane.setEditable(false);
    JScrollPane scrollPane = new JScrollPane(notesEditorPane);
    scrollPane.setPreferredSize(new Dimension(10, 120));
    northPanel.add(scrollPane, BorderLayout.CENTER);
    SwingUtilities.invokeLater(
        new Runnable() {
          @Override
          public void run() {
            notesEditorPane.scrollRectToVisible(new Rectangle(0, 0, 1, 1));
          }
        });

    getContentPane().add(northPanel, BorderLayout.NORTH);

    final JTabbedPane tabbedPane = new JTabbedPane();
    tabbedPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10));

    final JPanel generalInfoPanel = new JPanel(new BorderLayout());
    final Box box = Box.createVerticalBox();
    row = Box.createHorizontalBox();
    row.add(new JLabel(Language.getText("translationTool.tab.generalInfo.languageFileVersion")));
    final JTextField languageFileVersionTextField = new JTextField(20);
    row.add(languageFileVersionTextField);
    final JButton setCurrentVersionButton = new JButton();
    GuiUtils.updateButtonText(
        setCurrentVersionButton, "translationTool.tab.generalInfo.setCurrentVersionButton");
    row.add(setCurrentVersionButton);
    box.add(row);
    row = Box.createHorizontalBox();
    row.add(new JLabel(Language.getText("translationTool.tab.generalInfo.languageFileSubversion")));
    final JTextField languageFileSubversionTextField = new JTextField(20);
    row.add(languageFileSubversionTextField);
    row.add(new JLabel());
    setCurrentVersionButton.addActionListener(
        new ActionListener() {
          @Override
          public void actionPerformed(final ActionEvent event) {
            languageFileVersionTextField.setText(Consts.APPLICATION_LANGUAGE_VERSION);
            languageFileSubversionTextField.setText("1");
          }
        });
    box.add(row);
    row = Box.createHorizontalBox();
    row.add(new JLabel(Language.getText("translationTool.tab.generalInfo.translatorFirstName")));
    final JTextField translatorFirstNameTextField = new JTextField(20);
    row.add(translatorFirstNameTextField);
    row.add(new JLabel());
    box.add(row);
    row = Box.createHorizontalBox();
    row.add(new JLabel(Language.getText("translationTool.tab.generalInfo.translatorLastName")));
    final JTextField translatorLastNameTextField = new JTextField(20);
    row.add(translatorLastNameTextField);
    row.add(new JLabel());
    box.add(row);
    // Date/time formats
    final String dateTimeFormatToolTip =
        Language.getText("miscSettings.customDateTimeFormatToolTip");
    row = Box.createHorizontalBox();
    row.add(new JLabel(Language.getText("translationTool.tab.generalInfo.dateFormat")));
    final JComboBox<String> dateFormatTextComboBox =
        GuiUtils.createPredefinedListComboBox(PredefinedList.CUSTOM_DATE_FORMAT, false);
    dateFormatTextComboBox.setToolTipText(dateTimeFormatToolTip);
    row.add(dateFormatTextComboBox);
    final JPanel dateControlPanel = new JPanel(new BorderLayout());
    final JButton testDateButton = new JButton(Language.getText("miscSettings.testFormatButton"));
    dateControlPanel.add(testDateButton, BorderLayout.CENTER);
    dateControlPanel.add(GuiUtils.createDateTimeFormatHelpLinkLabel(), BorderLayout.EAST);
    row.add(dateControlPanel);
    box.add(row);
    row = Box.createHorizontalBox();
    row.add(new JLabel(Language.getText("translationTool.tab.generalInfo.timeFormat")));
    final JComboBox<String> timeFormatTextComboBox =
        GuiUtils.createPredefinedListComboBox(PredefinedList.CUSTOM_TIME_FORMAT, false);
    timeFormatTextComboBox.setToolTipText(dateTimeFormatToolTip);
    row.add(timeFormatTextComboBox);
    final JPanel timeControlPanel = new JPanel(new BorderLayout());
    final JButton testTimeButton = new JButton(Language.getText("miscSettings.testFormatButton"));
    timeControlPanel.add(testTimeButton, BorderLayout.CENTER);
    timeControlPanel.add(GuiUtils.createDateTimeFormatHelpLinkLabel(), BorderLayout.EAST);
    row.add(timeControlPanel);
    box.add(row);
    row = Box.createHorizontalBox();
    row.add(new JLabel(Language.getText("translationTool.tab.generalInfo.dateTimeFormat")));
    final JComboBox<String> dateTimeFormatTextComboBox =
        GuiUtils.createPredefinedListComboBox(PredefinedList.CUSTOM_DATE_TIME_FORMAT, false);
    dateTimeFormatTextComboBox.setToolTipText(dateTimeFormatToolTip);
    row.add(dateTimeFormatTextComboBox);
    final JPanel dateTimeControlPanel = new JPanel(new BorderLayout());
    final JButton testDateTimeButton =
        new JButton(Language.getText("miscSettings.testFormatButton"));
    dateTimeControlPanel.add(testDateTimeButton, BorderLayout.CENTER);
    dateTimeControlPanel.add(GuiUtils.createDateTimeFormatHelpLinkLabel(), BorderLayout.EAST);
    row.add(dateTimeControlPanel);
    final ActionListener testDateTimeActionListener =
        new ActionListener() {
          @Override
          public void actionPerformed(final ActionEvent event) {
            final String pattern =
                (event.getSource() == testDateButton
                        ? dateFormatTextComboBox
                        : event.getSource() == testTimeButton
                            ? timeFormatTextComboBox
                            : dateTimeFormatTextComboBox)
                    .getSelectedItem()
                    .toString();
            try {
              final String currentTime = new SimpleDateFormat(pattern).format(new Date());
              GuiUtils.showInfoDialog(
                  new Object[] {
                    Language.getText("miscSettings.dateTimeFormatValid"),
                    " ",
                    Language.getText("miscSettings.currentDateTimeWithFormat"),
                    currentTime
                  });
            } catch (IllegalArgumentException iae) {
              iae.printStackTrace();
              GuiUtils.showErrorDialog(Language.getText("miscSettings.dateTimeFormatInvalid"));
            }
          }
        };
    testDateButton.addActionListener(testDateTimeActionListener);
    testTimeButton.addActionListener(testDateTimeActionListener);
    testDateTimeButton.addActionListener(testDateTimeActionListener);
    box.add(row);
    row = Box.createHorizontalBox();
    row.add(new JLabel(Language.getText("translationTool.tab.generalInfo.personNameFormat")));
    final JComboBox<String> personNameFormatComboBox =
        new JComboBox<>(
            new String[] {
              Language.getText(
                  "translationTool.tab.generalInfo.personNameFormat.firstNameLastName"),
              Language.getText("translationTool.tab.generalInfo.personNameFormat.lastNameFirstName")
            });
    row.add(personNameFormatComboBox);
    row.add(new JLabel());
    box.add(row);
    GuiUtils.alignBox(box, 3);
    generalInfoPanel.add(new JScrollPane(GuiUtils.wrapInPanel(box)), BorderLayout.CENTER);
    GuiUtils.addNewTab(
        Language.getText("translationTool.tab.generalInfo.title"),
        Icons.INFORMATION_BALLOON,
        false,
        tabbedPane,
        generalInfoPanel,
        null);

    final JPanel textsPanel = new JPanel(new BorderLayout());
    final JProgressBar progressBar = new JProgressBar(0, Language.DEFAULT_LANGUAGE.textMap.size());
    progressBar.setStringPainted(true);
    textsPanel.add(progressBar, BorderLayout.NORTH);
    final JTable textsTable =
        new JTable() {
          // Custom cell renderer because we want icons and html source to be rendered...
          final DefaultTableCellRenderer customCellRenderer =
              new DefaultTableCellRenderer() {
                @Override
                public Component getTableCellRendererComponent(
                    final JTable table,
                    final Object value,
                    final boolean isSelected,
                    final boolean hasFocus,
                    final int row,
                    final int column) {
                  super.getTableCellRendererComponent(
                      table, value, isSelected, hasFocus, row, column);
                  Icon icon = null;
                  if (value != null && ((String) value).startsWith("<html>")) {
                    icon = Icons.HTML;
                    setText(' ' + (String) value);
                  }
                  if (value != null && ((String) value).indexOf('$') >= 0)
                    icon =
                        icon == null
                            ? Icons.DOCUMENT_ATTRIBUTE_P
                            : GuiUtils.concatenateIcons(icon, Icons.DOCUMENT_ATTRIBUTE_P);
                  if (value != null && ((String) value).indexOf('_') >= 0)
                    icon =
                        icon == null
                            ? Icons.KEYBOARD
                            : GuiUtils.concatenateIcons(icon, Icons.KEYBOARD);
                  setIcon(icon);
                  return this;
                }
              };

          @Override
          public boolean isCellEditable(final int row, final int column) {
            return column == TRANSLATION_COLUMN;
          }

          int tipCounter = 0; // To generate unique tips so it will follow the mouse cursor

          @Override
          public String getToolTipText(final MouseEvent event) {
            if (columnAtPoint(event.getPoint()) == TRANSLATION_COLUMN)
              return Language.getText("translationTool.tab.texts.table.toolTip")
                  + ((tipCounter++ & 0x01) == 1 ? " " : " ");

            return super.getToolTipText(event);
          }

          @Override
          public TableCellRenderer getCellRenderer(final int row, final int column) {
            return customCellRenderer;
          }
        };
    textsTable.setAutoCreateRowSorter(true);
    textsTable.setColumnSelectionAllowed(true);
    textsTable.setShowVerticalLines(true);
    textsTable.setPreferredScrollableViewportSize(new Dimension(850, 350));
    textsTable.getTableHeader().setReorderingAllowed(false);
    ((DefaultTableModel) textsTable.getModel())
        .setDataVector(new Vector<Vector<String>>(), TEXTS_HEADER_NAME_VECTOR);
    // Start editing for 1 click:
    ((DefaultCellEditor) textsTable.getDefaultEditor(textsTable.getColumnClass(TRANSLATION_COLUMN)))
        .setClickCountToStart(1);
    // Gain focus and cursor when editing started due to typing
    textsTable.setSurrendersFocusOnKeystroke(true);
    final TableBox tableBox = new TableBox(textsTable, getLayeredPane(), null);
    tableBox.getFilterComponentsWrapper().add(Box.createHorizontalStrut(10));
    final JCheckBox showOnlyUntranslatedCheckBox =
        new JCheckBox(Language.getText("translationTool.tab.texts.showOnlyUntranslated"));
    showOnlyUntranslatedCheckBox.addActionListener(
        new ActionListener() {
          @Override
          public void actionPerformed(final ActionEvent event) {
            tableBox.fireAdditionalRowFilterChanged();
          }
        });
    tableBox.getFilterComponentsWrapper().add(showOnlyUntranslatedCheckBox);
    tableBox.getFilterComponentsWrapper().add(Box.createHorizontalStrut(5));
    final JButton previousButton = new JButton(Icons.ARROW_180);
    GuiUtils.updateButtonText(previousButton, "translationTool.tab.texts.previousButton");
    tableBox.getFilterComponentsWrapper().add(previousButton);
    previousButton.setToolTipText(
        Language.getText("translationTool.tab.texts.previousButtonToolTip"));
    final JButton nextButton = new JButton(Icons.ARROW);
    nextButton.setHorizontalTextPosition(SwingConstants.LEFT);
    GuiUtils.updateButtonText(nextButton, "translationTool.tab.texts.nextButton");
    nextButton.setToolTipText(Language.getText("translationTool.tab.texts.nextButtonToolTip"));
    tableBox.getFilterComponentsWrapper().add(nextButton);
    final ActionListener prevNextActionListener =
        new ActionListener() {
          @Override
          public void actionPerformed(final ActionEvent event) {
            final int direction = event.getSource() == previousButton ? -1 : 1;
            // Visible rows count
            final int rowsCount = textsTable.getRowCount();
            if (rowsCount == 0) return; // No visible rows

            if (textsTable.isEditing()) textsTable.getCellEditor().stopCellEditing();

            int start = textsTable.getSelectedRow();
            if (start < 0) start = 0;
            int i = start;
            do {
              i += direction;
              if (i == rowsCount) i = 0;
              if (i < 0) i = rowsCount - 1;
              final String translation = (String) textsTable.getValueAt(i, TRANSLATION_COLUMN);
              if (translation == null || translation.length() == 0) {
                // select row (and column)
                textsTable.getSelectionModel().setSelectionInterval(i, i);
                textsTable.setColumnSelectionInterval(TRANSLATION_COLUMN, TRANSLATION_COLUMN);
                textsTable.scrollRectToVisible(textsTable.getCellRect(i, TRANSLATION_COLUMN, true));
                break;
              }
            } while (i != start);
            final String translation = (String) textsTable.getValueAt(i, TRANSLATION_COLUMN);
            if (i == start && translation != null && translation.length() > 0)
              GuiUtils.showInfoDialog(
                  Language.getText("translationTool.tab.texts.allDisplayedTextsAreTranslated"));
            textsTable.requestFocusInWindow();
          }
        };
    previousButton.addActionListener(prevNextActionListener);
    nextButton.addActionListener(prevNextActionListener);
    tableBox.getFilterComponentsWrapper().add(new JLabel("<html></html>"));
    textsPanel.add(tableBox, BorderLayout.CENTER);
    final Box southBox = Box.createVerticalBox();
    final JPanel previewPanel = new JPanel(new GridLayout(1, 2));
    final JEditorPane originalTextPreviewPane = new JEditorPane();
    originalTextPreviewPane.setEditable(false);
    scrollPane = new JScrollPane(originalTextPreviewPane);
    scrollPane.setBorder(
        BorderFactory.createTitledBorder(
            Language.getText("translationTool.tab.texts.originalTextPreview")));
    scrollPane.setPreferredSize(new Dimension(10, 140));
    previewPanel.add(scrollPane);
    final JEditorPane translationPreviewPane = new JEditorPane();
    translationPreviewPane.setEditable(false);
    scrollPane = new JScrollPane(translationPreviewPane);
    scrollPane.setBorder(
        BorderFactory.createTitledBorder(
            Language.getText("translationTool.tab.texts.translationPreview")));
    scrollPane.setPreferredSize(new Dimension(10, 140));
    previewPanel.add(scrollPane);
    southBox.add(previewPanel);
    final JTextArea commentTextArea = new JTextArea(2, 1);
    commentTextArea.setEditable(false);
    scrollPane = new JScrollPane(commentTextArea);
    scrollPane.setBorder(
        BorderFactory.createTitledBorder(Language.getText("translationTool.tab.texts.comments")));
    southBox.add(scrollPane);
    textsPanel.add(southBox, BorderLayout.SOUTH);
    GuiUtils.addNewTab(
        Language.getText("translationTool.tab.texts.title"),
        Icons.BALLOONS,
        false,
        tabbedPane,
        textsPanel,
        null);

    getContentPane().add(tabbedPane, BorderLayout.CENTER);

    final Runnable updateProgressBarTask =
        new Runnable() {
          @Override
          public void run() {
            @SuppressWarnings("unchecked")
            final Vector<Vector<String>> dataVector =
                ((DefaultTableModel) textsTable.getModel()).getDataVector();

            int translatedCount = 0;
            for (final Vector<String> row : dataVector) {
              final String translation = row.get(TRANSLATION_COLUMN);
              if (translation != null && translation.length() > 0) translatedCount++;
            }
            progressBar.setValue(translatedCount);
            progressBar.setString(
                Language.getText(
                    "translationTool.tab.texts.translationProgress",
                    translatedCount,
                    TOTAL_TEXTS_COUNT,
                    100 * translatedCount / TOTAL_TEXTS_COUNT));
          }
        };

    final Task<Holder<String>> updateTranslationPreviewTask =
        new Task<Holder<String>>() {
          /**
           * @param translationHolder if provided, it will be used as the translation; else the
           *     translation from the table will be read (from the selected row)
           */
          @Override
          public void execute(final Holder<String> translationHolder) {
            int selectedRow = -1;
            if (translationHolder != null || (selectedRow = textsTable.getSelectedRow()) >= 0) {
              String translation =
                  translationHolder == null
                      ? (String) textsTable.getValueAt(selectedRow, TRANSLATION_COLUMN)
                      : translationHolder.value;

              // If text has a mnemonic, show it as HTML text where the mnemonic is underlined:
              int underScoreIndex;
              if (translation != null
                  && (underScoreIndex = translation.indexOf('_')) >= 0
                  && underScoreIndex < translation.length() - 1)
                translation =
                    "<html>"
                        + translation.substring(0, underScoreIndex)
                        + "<u>"
                        + translation.charAt(underScoreIndex + 1)
                        + "</u>"
                        + translation.substring(underScoreIndex + 2)
                        + "</html>";

              translationPreviewPane.setContentType(
                  translation != null && translation.startsWith("<html>")
                      ? "text/html"
                      : "text/plain");
              translationPreviewPane.setText(translation);
              SwingUtilities.invokeLater(
                  new Runnable() {
                    @Override
                    public void run() {
                      translationPreviewPane.scrollRectToVisible(new Rectangle(0, 0, 1, 1));
                    }
                  });
            } else {
              translationPreviewPane.setContentType("text/plain");
              translationPreviewPane.setText(null);
            }
          }
        };

    textsTable
        .getSelectionModel()
        .addListSelectionListener(
            new ListSelectionListener() {
              @Override
              public void valueChanged(final ListSelectionEvent event) {
                // Update translation preview
                updateTranslationPreviewTask.execute(null);

                // Update original text preview and comments
                final int selectedRow = textsTable.getSelectedRow();
                if (selectedRow >= 0) {
                  // Update original text preview
                  String originalText =
                      (String) textsTable.getValueAt(selectedRow, ORIGINAL_TEXT_COLUMN);

                  // If text has a mnemonic, show it as HTML text where the mnemonic is underlined:
                  int underScoreIndex;
                  if (originalText != null
                      && (underScoreIndex = originalText.indexOf('_')) >= 0
                      && underScoreIndex < originalText.length() - 1)
                    originalText =
                        "<html>"
                            + originalText.substring(0, underScoreIndex)
                            + "<u>"
                            + originalText.charAt(underScoreIndex + 1)
                            + "</u>"
                            + originalText.substring(underScoreIndex + 2)
                            + "</html>";

                  originalTextPreviewPane.setContentType(
                      originalText != null && originalText.startsWith("<html>")
                          ? "text/html"
                          : "text/plain");
                  originalTextPreviewPane.setText(originalText);
                  SwingUtilities.invokeLater(
                      new Runnable() {
                        @Override
                        public void run() {
                          originalTextPreviewPane.scrollRectToVisible(new Rectangle(0, 0, 1, 1));
                        }
                      });

                  // Update comments
                  commentTextArea.setText(null);
                  final String textKey =
                      (String) textsTable.getValueAt(selectedRow, TEXT_KEY_COLUMN);
                  // Text group comments:
                  boolean firstGroupComment = true;
                  int dotIndex = 0;
                  while ((dotIndex = textKey.indexOf('.', dotIndex)) >= 0) {
                    final String groupComment =
                        Language.DEFAULT_LANGUAGE.textGroupCommentsMap.get(
                            textKey.substring(0, dotIndex));
                    if (groupComment != null) {
                      if (firstGroupComment) firstGroupComment = false;
                      else commentTextArea.append(" => ");
                      commentTextArea.append(groupComment);
                    }
                    dotIndex++;
                  }
                  if (!firstGroupComment) commentTextArea.append("\n");
                  // Text comment
                  commentTextArea.append(
                      Language.DEFAULT_LANGUAGE.textCommentsMap.get(
                          textKey)); // TextArea.append() ommits nulls
                  commentTextArea.setCaretPosition(0);
                } else {
                  originalTextPreviewPane.setContentType("text/plain");
                  originalTextPreviewPane.setText(null);

                  commentTextArea.setText(null);
                }
              }
            });

    // Show real time preview while editing:
    ((JTextField)
            ((DefaultCellEditor)
                    textsTable.getDefaultEditor(textsTable.getColumnClass(TRANSLATION_COLUMN)))
                .getComponent())
        .getDocument()
        .addDocumentListener(
            new DocumentListener() {
              @Override
              public void removeUpdate(final DocumentEvent event) {
                changedUpdate(event);
              }

              @Override
              public void insertUpdate(final DocumentEvent event) {
                changedUpdate(event);
              }

              @Override
              public void changedUpdate(final DocumentEvent event) {
                try {
                  final String translation =
                      event.getDocument().getText(0, event.getDocument().getLength());
                  updateTranslationPreviewTask.execute(new Holder<String>(translation));
                } catch (final BadLocationException ble) {
                  ble.printStackTrace();
                }
              }
            });

    textsTable
        .getModel()
        .addTableModelListener(
            new TableModelListener() {
              @Override
              public void tableChanged(final TableModelEvent event) {
                // event.getColumn() returns the column model index
                if (event.getColumn() >= 0 && event.getColumn() == TRANSLATION_COLUMN) {
                  updateProgressBarTask.run();
                  if (showOnlyUntranslatedCheckBox.isSelected()) {
                    // If only untranslated texts are displayed, then after editing the current row
                    // will be hidden,
                    // so the selected row have to be moved up by 1
                    // (If the entered text will be empty, then this will have a side effect of not
                    // changing the selected row
                    // but that's not really a problem, it's even the intended operation.)
                    final int newSelectedRow = Math.max(0, textsTable.getEditingRow());
                    tableBox.fireAdditionalRowFilterChanged();
                    textsTable
                        .getSelectionModel()
                        .setSelectionInterval(newSelectedRow, newSelectedRow);
                  }
                }
              }
            });

    editedLanguageComboBox.addActionListener(
        new ActionListener() {
          {
            actionPerformed(null);
          } // Initialize

          @Override
          public void actionPerformed(final ActionEvent event) {
            final String selectedLanguage = (String) editedLanguageComboBox.getSelectedItem();
            if (selectedLanguage == null) return;
            // selectedLanguage can be null when the combo box is being rebuilt...
            final boolean isLanguageSelected =
                selectedLanguage != null && !EMPTY_LANGUAGE.equals(selectedLanguage);

            if (isLanguageSelected) {
              final Language language = Language.loadLanguage(selectedLanguage);
              if (language == null) {
                GuiUtils.showErrorDialog(
                    Language.getText("translationTool.failedToLoadLanguage", selectedLanguage));
                editedLanguageComboBox.setSelectedIndex(0);
                GuiUtils.setComponentTreeEnabled(tabbedPane, false);
                saveButton.setEnabled(false);
              } else {
                languageFileVersionTextField.setText(language.languageFileVersion);
                languageFileSubversionTextField.setText(language.languageFileSubversion);
                translatorFirstNameTextField.setText(language.translatorFirstName);
                translatorLastNameTextField.setText(language.translatorLastName);
                dateFormatTextComboBox.setSelectedItem(language.defaultDateFormatPattern);
                timeFormatTextComboBox.setSelectedItem(language.defaultTimeFormatPattern);
                dateTimeFormatTextComboBox.setSelectedItem(language.defaultDateTimeFormatPattern);
                personNameFormatComboBox.setSelectedIndex(
                    language.personNameFormatFirstNameFirst ? 0 : 1);

                final Vector<Vector<String>> dataVector =
                    new Vector<Vector<String>>(Language.DEFAULT_LANGUAGE.textMap.size());
                final Map<String, String> languageTextMap = language.textMap;
                for (final Entry<String, String> textEntry :
                    Language.DEFAULT_LANGUAGE.textMap.entrySet()) {
                  final Vector<String> row = new Vector<String>(3);

                  final String textKey = textEntry.getKey();
                  row.add(textKey);
                  row.add(textEntry.getValue());
                  row.add(languageTextMap.get(textKey));

                  dataVector.add(row);
                }

                ((DefaultTableModel) textsTable.getModel())
                    .setDataVector(dataVector, TEXTS_HEADER_NAME_VECTOR);
                textsTable
                    .getRowSorter()
                    .setSortKeys(Arrays.asList(new SortKey(0, SortOrder.ASCENDING)));
                tableBox.setAdditionalRowFilter(
                    new RowFilter<TableModel, Integer>() {
                      @Override
                      public boolean include(
                          final Entry<? extends TableModel, ? extends Integer> entry) {
                        if (showOnlyUntranslatedCheckBox.isSelected()) {
                          final String translation =
                              dataVector.get(entry.getIdentifier()).get(TRANSLATION_COLUMN);
                          return translation == null || translation.length() == 0;
                        } else return true;
                      }
                    });

                updateProgressBarTask.run();

                GuiUtils.setComponentTreeEnabled(tabbedPane, true);
                saveButton.setEnabled(true);
              }
            } else {
              GuiUtils.setComponentTreeEnabled(tabbedPane, false);
              saveButton.setEnabled(false);
            }
          }
        });

    final ActionListener saveActionListener =
        new ActionListener() {
          /** If <code>event</code> is <code>null</code> a new language file will be saved! */
          @Override
          public void actionPerformed(final ActionEvent event) {
            if (textsTable.isEditing()) textsTable.getCellEditor().stopCellEditing();

            try {
              final Document document =
                  DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
              final Element rootElement = document.createElement("language");
              rootElement.setAttribute(
                  Language.VERSION_ATTRIBUTE_NAME,
                  event == null
                      ? Consts.APPLICATION_LANGUAGE_VERSION
                      : languageFileVersionTextField.getText());
              rootElement.setAttribute(
                  Language.SUBVERSION_ATTRIBUTE_NAME,
                  event == null ? "1" : languageFileSubversionTextField.getText());
              rootElement.setAttribute(
                  Language.TRANSLATOR_FIRST_NAME_ATTRIBUTE_NAME,
                  event == null ? "" : translatorFirstNameTextField.getText());
              rootElement.setAttribute(
                  Language.TRANSLATOR_LAST_NAME_ATTRIBUTE_NAME,
                  event == null ? "" : translatorLastNameTextField.getText());

              final Element dateFormatElement =
                  document.createElement(Language.DATE_FORMAT_TAG_NAME);
              dateFormatElement.setTextContent(
                  event == null
                      ? Language.DEFAULT_LANGUAGE.defaultDateFormatPattern
                      : (String) dateFormatTextComboBox.getSelectedItem());
              rootElement.appendChild(dateFormatElement);

              final Element timeFormatElement =
                  document.createElement(Language.TIME_FORMAT_TAG_NAME);
              timeFormatElement.setTextContent(
                  event == null
                      ? Language.DEFAULT_LANGUAGE.defaultTimeFormatPattern
                      : (String) timeFormatTextComboBox.getSelectedItem());
              rootElement.appendChild(timeFormatElement);

              final Element dateTimeFormatElement =
                  document.createElement(Language.DATE_TIME_FORMAT_TAG_NAME);
              dateTimeFormatElement.setTextContent(
                  event == null
                      ? Language.DEFAULT_LANGUAGE.defaultDateTimeFormatPattern
                      : (String) dateTimeFormatTextComboBox.getSelectedItem());
              rootElement.appendChild(dateTimeFormatElement);

              final Element personNameFormatElement =
                  document.createElement(Language.PERSON_NAME_FORMAT_TAG_NAME);
              personNameFormatElement.setTextContent(
                  event == null
                      ? (Language.DEFAULT_LANGUAGE.personNameFormatFirstNameFirst
                          ? Language.PERSON_NAME_FORMAT_FIRST_NAME_LAST_NAME
                          : Language.PERSON_NAME_FORMAT_LAST_NAME_FISRT_NAME)
                      : personNameFormatComboBox.getSelectedIndex() == 0
                          ? Language.PERSON_NAME_FORMAT_FIRST_NAME_LAST_NAME
                          : Language.PERSON_NAME_FORMAT_LAST_NAME_FISRT_NAME);
              rootElement.appendChild(personNameFormatElement);

              if (event != null) {
                // Now add the texts.
                // First create the text groups
                // Create groups in the order of their node counts (for example "menu.file" has 2
                // nodes)
                final List<String> textGroupList =
                    new ArrayList<String>(Language.DEFAULT_LANGUAGE.textGroupCommentsMap.keySet());
                Collections.sort(
                    textGroupList,
                    new Comparator<String>() {
                      @Override
                      public int compare(final String g1, final String g2) {
                        int c1 = 0;
                        for (int i = g1.length() - 2; i > 0; i--) // Cannot start or end with '.'
                        if (g1.charAt(i) == '.') c1++;
                        int c2 = 0;
                        for (int i = g2.length() - 2; i > 0; i--) // Cannot start or end with '.'
                        if (g2.charAt(i) == '.') c2++;
                        return c1 - c2;
                      }
                    });
                final Map<String, Element> textGroupElementMap =
                    new HashMap<String, Element>(
                        textGroupList.size()); // Store the elements mapped from text group keys
                for (final String textGroup : textGroupList) {
                  // Check if there is a parent group
                  Element parentElement = null;
                  String relativeKey = null;
                  int dotIndex =
                      textGroup.length()
                          - 1; // The last node will be cut off (the parent cannot have the same
                               // key, do not check it)
                  while ((dotIndex = textGroup.lastIndexOf('.', dotIndex)) >= 0) {
                    parentElement = textGroupElementMap.get(textGroup.substring(0, dotIndex));
                    if (parentElement != null) {
                      relativeKey = textGroup.substring(dotIndex + 1);
                      break;
                    }
                    dotIndex--;
                  }

                  final Element groupElement = document.createElement(Language.TEXT_GROUP_TAG_NAME);
                  groupElement.setAttribute(
                      Language.KEY_ATTRIBUTE_NAME, parentElement == null ? textGroup : relativeKey);
                  (parentElement == null ? rootElement : parentElement).appendChild(groupElement);

                  textGroupElementMap.put(textGroup, groupElement);
                }

                // And finally create text elements
                @SuppressWarnings("unchecked")
                final Vector<Vector<String>> dataVector =
                    ((DefaultTableModel) textsTable.getModel()).getDataVector();
                for (final Vector<String> row : dataVector) {
                  final String translation = row.get(TRANSLATION_COLUMN);
                  if (translation == null || translation.length() == 0) continue;

                  final String key = row.get(TEXT_KEY_COLUMN);

                  // Find "closest" parent text group
                  Element parentElement = null;
                  String relativeKey = null;
                  int dotIndex =
                      key.length()
                          - 1; // The last node will be cut off (do not check the text key itself as
                               // a group key)
                  while ((dotIndex = key.lastIndexOf('.', dotIndex)) >= 0) {
                    parentElement = textGroupElementMap.get(key.substring(0, dotIndex));
                    if (parentElement != null) {
                      relativeKey = key.substring(dotIndex + 1);
                      break;
                    }
                    dotIndex--;
                  }

                  final Element textElement = document.createElement(Language.TEXT_TAG_NAME);
                  textElement.setAttribute(
                      Language.KEY_ATTRIBUTE_NAME, parentElement == null ? key : relativeKey);
                  textElement.setTextContent(translation);
                  (parentElement == null ? rootElement : parentElement).appendChild(textElement);
                }
              }

              document.appendChild(rootElement);

              final Transformer transformer = TransformerFactory.newInstance().newTransformer();
              transformer.setOutputProperty(OutputKeys.INDENT, "yes");
              transformer.transform(
                  new DOMSource(document),
                  new StreamResult(
                      new FileOutputStream(
                          Language.getLanguageFile(
                              event == null
                                  ? newLanguageNameTextField.getText()
                                  : (String) editedLanguageComboBox.getSelectedItem()))));

              if (event != null)
                GuiUtils.showInfoDialog(
                    Language.getText("translationTool.changesSavedSuccessfully"));

            } catch (final Exception e) {
              e.printStackTrace();
              GuiUtils.showErrorDialog(Language.getText("translationTool.failedToSaveTranslation"));
            }
          }
        };
    saveButton.addActionListener(saveActionListener);

    createNewLanguageButton.addActionListener(
        new ActionListener() {
          @Override
          public void actionPerformed(final ActionEvent event) {
            final String newLanguageName = newLanguageNameTextField.getText();
            if (newLanguageName.length() == 0) return;

            if (newLanguageName.indexOf(' ') >= 0) {
              GuiUtils.showErrorDialog(
                  Language.getText("translationTool.doNotUseSpacesInLanguageName"));
              return;
            }

            if (Language.getLanguageFile(newLanguageName).exists()) {
              GuiUtils.showErrorDialog(
                  Language.getText("translationTool.languageAlreadyExists", newLanguageName));
              return;
            }

            saveActionListener.actionPerformed(null);

            rebuildEditedLanguageComboBoxTask.run();
            editedLanguageComboBox.setSelectedItem(newLanguageName);

            tabbedPane.setSelectedIndex(0);
            translatorFirstNameTextField.requestFocusInWindow();
          }
        });

    maximizeWithMarginAndShow(30, null, editedLanguageComboBox, true);
  }
Exemplo n.º 6
0
  /** Creates a new WelcomeFrame and makes it visible. */
  public WelcomeFrame() {
    setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
    addWindowListener(
        new WindowAdapter() {
          public void windowClosing(final WindowEvent event) {
            cancel();
          };
        });

    setIconImage(Icons.SC2GEARS.getImage());

    final Box box = Box.createVerticalBox();
    box.add(Box.createVerticalStrut(5));
    box.add(GuiUtils.wrapInPanel(SharedUtils.createAnimatedLogoLabel()));
    box.add(Box.createVerticalStrut(10));
    GuiUtils.changeFontToBold(welcomeLabel);
    box.add(Box.createVerticalStrut(15));
    box.add(GuiUtils.wrapInPanel(welcomeLabel));
    box.add(Box.createVerticalStrut(15));
    box.add(GuiUtils.wrapInPanel(firstRunLabel));
    box.add(GuiUtils.wrapInPanel(chooseLabel));
    box.add(Box.createVerticalStrut(15));
    box.add(GuiUtils.wrapInPanel(thankYouLabel));
    box.add(Box.createVerticalStrut(15));

    final JPanel languagePanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 1));
    languagePanel.add(languageLabel);
    final JComboBox<String> languagesComboBox = new JComboBox<>(Language.getAvailableLanguages());
    languagesComboBox.setMaximumRowCount(
        languagesComboBox.getModel().getSize()); // Display all languages
    languagesComboBox.setRenderer(
        new BaseLabelListCellRenderer<String>() {
          @Override
          public Icon getIcon(final String value) {
            return Icons.getLanguageIcon(value);
          }
        });
    languagesComboBox.addActionListener(
        new ActionListener() {
          @Override
          public void actionPerformed(final ActionEvent event) {
            final String language = (String) languagesComboBox.getSelectedItem();
            Language.loadAndActivateLanguage(language);

            reassignTexts();
          }
        });
    languagePanel.add(languagesComboBox);
    box.add(languagePanel);

    final JPanel voicePanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 1));
    voicePanel.add(voiceLabel);
    final JComboBox<VoiceDescription> voiceComboBox = new JComboBox<>(Sounds.VOICE_DESCRIPTIONS);
    voiceComboBox.setMaximumRowCount(15); // Not too many languages, display them all
    voiceComboBox.setRenderer(
        new BaseLabelListCellRenderer<VoiceDescription>() {
          @Override
          public Icon getIcon(final VoiceDescription value) {
            return Icons.getLanguageIcon(value.language);
          }
        });
    voiceComboBox.addActionListener(
        new ActionListener() {
          @Override
          public void actionPerformed(final ActionEvent event) {
            final VoiceDescription voiceDescription =
                (VoiceDescription) voiceComboBox.getSelectedItem();
            Settings.set(Settings.KEY_SETTINGS_VOICE, voiceDescription.name);
            Sounds.playSoundSample(Sounds.SAMPLE_WELCOME, false);
          }
        });
    voicePanel.add(voiceComboBox);
    box.add(voicePanel);

    int maxWidth =
        Math.max(
            languagesComboBox.getPreferredSize().width, voiceComboBox.getPreferredSize().width);
    maxWidth += 5;
    languagesComboBox.setPreferredSize(
        new Dimension(maxWidth, languagesComboBox.getPreferredSize().height));
    voiceComboBox.setPreferredSize(
        new Dimension(maxWidth, voiceComboBox.getPreferredSize().height));

    box.add(Box.createVerticalStrut(15));

    final JPanel buttonsPanel = new JPanel();
    okButton.addActionListener(
        new ActionListener() {
          @Override
          public void actionPerformed(final ActionEvent event) {
            dispose();
            Settings.set(
                Settings.KEY_SETTINGS_LANGUAGE, (String) languagesComboBox.getSelectedItem());
            Settings.saveProperties();
            synchronized (WelcomeFrame.this) {
              Sounds.playSoundSample(Sounds.SAMPLE_THANK_YOU, false);
              WelcomeFrame.this
                  .notify(); // Notify the main thread to continue starting the application
            }
          }
        });
    buttonsPanel.add(okButton);
    cancelButton.addActionListener(
        new ActionListener() {
          @Override
          public void actionPerformed(final ActionEvent event) {
            cancel();
          }
        });
    buttonsPanel.add(cancelButton);
    box.add(buttonsPanel);

    box.add(Box.createVerticalStrut(15));

    final JPanel panel = new JPanel();
    panel.add(Box.createHorizontalStrut(15));
    panel.add(box);
    panel.add(Box.createHorizontalStrut(15));
    getContentPane().add(panel);

    setResizable(false);

    reassignTexts();
    setVisible(true);

    okButton.requestFocusInWindow();
    Sounds.playSoundSample(Sounds.SAMPLE_WELCOME, false);
  }
Exemplo n.º 7
0
/**
 * The Parsing service servlet.
 *
 * @author Andras Belicza
 */
@SuppressWarnings("serial")
public class ParsingServlet extends BaseServlet {

  private static final Logger LOGGER = Logger.getLogger(ParsingServlet.class.getName());

  private static final String DATE_TIME_FORMAT_PATTERN = "yyyy-MM-dd HH:mm:ss";
  private static final DateFormat DATE_TIME_FORMAT = new SimpleDateFormat(DATE_TIME_FORMAT_PATTERN);
  private static final String TEXT_UNKNOWN = Language.getText("general.unknown");
  private static String[] ACTION_TYPE_STRINGS;

  static {
    loadResources();
  }

  private static class ResponseWrapper extends HttpServletResponseWrapper {

    private int status = HttpServletResponse.SC_OK;

    /**
     * Creates a new ResponseWrapper.
     *
     * @param response
     */
    public ResponseWrapper(final HttpServletResponse response) {
      super(response);
    }

    @Override
    public void setStatus(final int sc) {
      status = sc;
      super.setStatus(sc);
    }

    @SuppressWarnings("deprecation") // This is due to Tomcat 7 (not applicable to AppEngine)
    @Override
    public void setStatus(final int sc, final String sm) {
      status = sc;
      super.setStatus(sc, sm);
    }

    @Override
    public void sendError(final int sc) throws IOException {
      status = sc;
      super.sendError(sc);
    }

    @Override
    public void sendError(final int sc, final String msg) throws IOException {
      status = sc;
      super.sendError(sc, msg);
    }

    /**
     * Tells if error is returned by comparing the {@link #status} to {@link
     * HttpServletResponse#SC_OK}.
     *
     * @return true if error is returned; false otherwise
     */
    public boolean isError() {
      return status != HttpServletResponse.SC_OK;
    }
  }

  public static void loadResources() {
    if (ACTION_TYPE_STRINGS != null) return; // Already loaded

    // Ensure ability codes repository is initialized when it is needed:
    try {
      Class.forName(ReplayUtils.class.getName());

      final ActionType[] actionTypeValues = ActionType.values();
      ACTION_TYPE_STRINGS = new String[actionTypeValues.length];
      for (int i = 0; i < ACTION_TYPE_STRINGS.length; i++)
        ACTION_TYPE_STRINGS[i] = Character.toString(actionTypeValues[i].stringValue.charAt(0));

    } catch (final ClassNotFoundException cnfe) {
      LOGGER.log(
          Level.SEVERE, "Exception during initialization (processing ReplayUtils class)!", cnfe);
    }
  }

  /*
   * doPost() is more common in case of this servlet, so doGet() calls doPost().
   */
  @Override
  protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
      throws ServletException, IOException {
    doPost(request, response);
  }

  @Override
  protected void doPost(final HttpServletRequest request, final HttpServletResponse response)
      throws ServletException, IOException {
    final long startNanoTime = System.nanoTime();

    final String operation = checkProtVerAndGetOperation(PROTOCOL_VERSION_1, request, response);
    if (operation == null) return;

    final String apiKey = request.getParameter(PARAM_API_KEY);
    if (apiKey == null || apiKey.isEmpty()) {
      LOGGER.warning("Missing API key!");
      response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing API key!");
      return;
    }

    long opsCharged = 0;
    ApiAccount apiAccount = null;
    PersistenceManager pm = null;
    ResponseWrapper responseWrapper = null;
    boolean denied = false;

    try {
      pm = PMF.get().getPersistenceManager();

      // Check API key
      final List<ApiAccount> apiAccountList =
          new JQBuilder<>(pm, ApiAccount.class).filter("apiKey==p1", "String p1").get(apiKey);
      if (apiAccountList.isEmpty()) {
        LOGGER.warning("Unauthorized access, invalid API Key: " + apiKey);
        response.sendError(
            HttpServletResponse.SC_FORBIDDEN, "Unauthorized access, invalid API Key!");
        return;
      }
      apiAccount = apiAccountList.get(0);

      responseWrapper = new ResponseWrapper(response);

      // Check Ops quota
      final List<ApiCallStat> totalApiCallStatList =
          new JQBuilder<>(pm, ApiCallStat.class)
              .filter("ownerKey==p1 && day==p2", "KEY p1, String p2")
              .get(apiAccount.getKey(), ApiCallStat.DAY_TOTAL);
      final long totalUsedOps =
          totalApiCallStatList.isEmpty() ? 0 : totalApiCallStatList.get(0).getUsedOps();
      if (!OPERATION_INFO.equals(operation) && totalUsedOps >= apiAccount.getPaidOps()) {
        denied = true;
        LOGGER.warning(
            "Ops quota have been exceeded, serving denied! (API account: "
                + apiAccount.getUser().getEmail()
                + ")");
        responseWrapper.sendError(
            HttpServletResponse.SC_PAYMENT_REQUIRED,
            "Ops quota have been exceeded, serving denied!");
        return;
      }

      switch (operation) {
        case OPERATION_INFO:
          opsCharged = infoOp(request, responseWrapper, pm, apiAccount);
          break;
        case OPERATION_MAP_INFO:
          opsCharged = mapInfoOp(request, responseWrapper, pm, apiAccount);
          break;
        case OPERATION_PARSE_REPLAY:
          opsCharged = parseRepOp(request, responseWrapper, pm, apiAccount);
          break;
        case OPERATION_PROFILE_INFO:
          opsCharged = profInfoOp(request, responseWrapper, pm, apiAccount);
          break;
        default:
          LOGGER.warning(
              "Invalid Operation! (API account: " + apiAccount.getUser().getEmail() + ")");
          responseWrapper.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid Operation!");
          return;
      }

      // Notification available Ops will be checked in the task servlet, update API call stat task

    } finally {
      if (apiAccount != null)
        TaskServlet.register_updateApiCallStat(
            apiAccount.getKey(),
            apiAccount.getPaidOps(),
            apiAccount.getNotificationAvailOps(),
            opsCharged,
            (System.nanoTime() - startNanoTime) / 1000000l,
            denied,
            responseWrapper == null ? true : responseWrapper.isError(),
            operation);
      if (pm != null) pm.close();
    }
  }

  /**
   * XML builder.
   *
   * @author Andras Belicza
   */
  private static class XmlBuilder {

    /** The document we build. */
    private final Document document;
    /** Parent element where new elements are attached to. */
    private Element parentElement;

    /**
     * Creates a new XmlBuilder.
     *
     * @param docVersion document version to set as the {@link ParsingServletApi#XATTR_DOC_VERSION}
     *     value for the {@link ParsingServletApi#XTAG_RESPONSE} root element
     */
    public XmlBuilder(final String docVersion) throws ParserConfigurationException {
      document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
      // Create root element
      parentElement = document.createElement(XTAG_RESPONSE);
      parentElement.setAttribute(XATTR_DOC_VERSION, docVersion);
      document.appendChild(parentElement);
    }

    /**
     * Sets the parent element where new elements are attached to.
     *
     * @param parentElement parent element to be set; can be <code>null</code> in which case the
     *     root element will be set
     * @return the parent element
     */
    public Element setParentElement(final Element parentElement) {
      return this.parentElement =
          parentElement == null ? document.getDocumentElement() : parentElement;
    }

    /**
     * Returns the parent element where new elements are attached to.
     *
     * @return the parent element where new elements are attached to
     */
    public Element getParentElement() {
      return parentElement;
    }

    /**
     * Creates and attaches a new element to the parent element.
     *
     * @param elementName name of the new element to be created and attached
     * @return the created and attached new element
     */
    public Element createElement(final String elementName) {
      return createElement(elementName, null, null);
    }

    /**
     * Creates and attaches a new element to the parent element with a {@link
     * ParsingServletApi#XATTR_VALUE} attribute having the specified value.
     *
     * @param elementName name of the new element to be created and attached
     * @param valueAttrValue value of {@link ParsingServletApi#XATTR_VALUE} attribute to be set for
     *     the new element
     * @return the created and attached new element
     */
    public Element createElement(final String elementName, final Object valueAttrValue) {
      return createElement(elementName, XATTR_VALUE, valueAttrValue);
    }

    /**
     * Creates and attaches a new element to the parent element with an attribute.
     *
     * @param elementName name of the new element to be created and attached
     * @param attrName name of the attribute to be set
     * @param attrValue value of the attribute to set; can be <code>null</code> in which case no
     *     attribute will be set
     * @return the created and attached new element
     */
    public Element createElement(
        final String elementName, final String attrName, final Object attrValue) {
      final Element element = document.createElement(elementName);
      if (attrValue != null) element.setAttribute(attrName, attrValue.toString());
      parentElement.appendChild(element);
      return element;
    }

    /**
     * Creates a {@link ParsingServletApi#XTAG_RESULT} tag from the specified result and attaches it
     * to the parent element .
     *
     * @param result result to create an XML tag from
     */
    public void createResultElement(final IResult result) {
      createElement(XTAG_RESULT, XATTR_CODE, result.getCode()).setTextContent(result.getMessage());
    }

    /**
     * Creates and attaches a new element to the parent element with a value attribute from a {@link
     * Date}. Also attaches a {@link ParsingServletApi#XATTR_PATTERN} describing the date-time
     * pattern used in the value attribute.
     *
     * @param result result to create an XML tag from
     * @param elementName
     */
    public void createDateTimeElement(final String elementName, final Date date) {
      createElement(elementName, DATE_TIME_FORMAT.format(date))
          .setAttribute(XATTR_PATTERN, DATE_TIME_FORMAT_PATTERN);
    }

    /**
     * Prints the document to the specified HTTP servlet response.
     *
     * @param response response to print the document to
     */
    public void printDocument(final HttpServletResponse response)
        throws TransformerFactoryConfigurationError, TransformerException, IOException {
      response.setContentType("text/xml");
      response.setCharacterEncoding("UTF-8");
      setNoCache(response);

      final Transformer transformer = TransformerFactory.newInstance().newTransformer();
      transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
      transformer.transform(new DOMSource(document), new StreamResult(response.getOutputStream()));
    }
  }

  /** Info operation. */
  private long infoOp(
      final HttpServletRequest request,
      final HttpServletResponse response,
      final PersistenceManager pm,
      final ApiAccount apiAccount)
      throws IOException {
    Integer daysCount = getIntParam(request, PARAM_DAYS_COUNT);
    LOGGER.fine("API account: " + apiAccount.getUser().getEmail() + ", days count: " + daysCount);

    if (daysCount != null) {
      if (daysCount < 0 || daysCount > 14) {
        LOGGER.warning("Invalid days count, must be between 0 and 14!");
        response.sendError(
            HttpServletResponse.SC_BAD_REQUEST, "Invalid days count, must be between 0 and 14!");
        return 0;
      }
    } else daysCount = 2;

    try {
      // Total + days count (last days count)
      final List<ApiCallStat> apiCallStatList =
          new JQBuilder<>(pm, ApiCallStat.class)
              .filter("ownerKey==p1 && day==p2", "KEY p1")
              .range(0, daysCount + 1)
              .desc("day")
              .get(apiAccount.getKey());

      final XmlBuilder xb = new XmlBuilder("1.0");

      xb.createResultElement(InfoResult.OK);
      xb.createElement(
          XTAG_ENGINE_VER,
          ReplayFactory
              .getVersion()); // DO NOT USE ReplayFactory.VERSION because the compiler replaces the
                              // actual ReplayFactory.VERSION and in live environment not the value
                              // from sc2gears-parsing-engine.jar will be used!
      xb.createDateTimeElement(XTAG_SERVER_TIME, new Date());

      xb.createElement(XTAG_PAID_OPS, apiAccount.getPaidOps());
      xb.createElement(
          XTAG_AVAIL_OPS,
          apiCallStatList.isEmpty()
              ? apiAccount.getPaidOps()
              : apiAccount.getPaidOps() - apiCallStatList.get(0).getUsedOps());

      final Element callStatsElement =
          xb.createElement(XTAG_CALL_STATS, XATTR_COUNT, apiCallStatList.size());
      callStatsElement.setAttribute(XATTR_PATTERN, ApiCallStat.DAY_PATTERN);
      for (final ApiCallStat apiCallStat : apiCallStatList) {
        xb.setParentElement(callStatsElement);
        xb.setParentElement(xb.createElement(XTAG_CALL_STAT, XATTR_DAY, apiCallStat.getDay()));
        xb.createElement(XTAG_API_CALLS, apiCallStat.getCalls());
        xb.createElement(XTAG_USED_OPS, apiCallStat.getUsedOps());
        xb.createElement(XTAG_AVG_EXEC_TIME, apiCallStat.getAvgExecTime())
            .setAttribute(XATTR_UNIT, "ms");
        xb.createElement(XTAG_DENIED_CALLS, apiCallStat.getDeniedCalls());
        xb.createElement(XTAG_ERRORS, apiCallStat.getErrors());

        xb.createElement(XTAG_INFO_CALLS, apiCallStat.getInfoCalls());
        xb.createElement(XTAG_AVG_INFO_EXEC_TIME, apiCallStat.getAvgInfoExecTime())
            .setAttribute(XATTR_UNIT, "ms");
        xb.createElement(XTAG_MAP_INFO_CALLS, apiCallStat.getMapInfoCalls());
        xb.createElement(XTAG_AVG_MAP_INFO_EXEC_TIME, apiCallStat.getAvgMapInfoExecTime())
            .setAttribute(XATTR_UNIT, "ms");
        xb.createElement(XTAG_PARSE_REP_CALLS, apiCallStat.getParseRepCalls());
        xb.createElement(XTAG_AVG_PARSE_REP_EXEC_TIME, apiCallStat.getAvgParseRepExecTime())
            .setAttribute(XATTR_UNIT, "ms");
        xb.createElement(XTAG_PROF_INFO_CALLS, apiCallStat.getProfInfoCalls());
        xb.createElement(XTAG_AVG_PROF_INFO_EXEC_TIME, apiCallStat.getAvgProfInfoExecTime())
            .setAttribute(XATTR_UNIT, "ms");
      }

      xb.printDocument(response);

      return 0;
    } catch (final Exception e) {
      LOGGER.log(Level.SEVERE, "", e);
      response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
      return 0;
    }
  }

  /** Get map info operation. */
  private long mapInfoOp(
      final HttpServletRequest request,
      final HttpServletResponse response,
      final PersistenceManager pm,
      final ApiAccount apiAccount)
      throws IOException {
    String mapFileName = request.getParameter(PARAM_MAP_FILE_NAME);

    LOGGER.fine(
        "API account: " + apiAccount.getUser().getEmail() + ", map file name: " + mapFileName);

    if (mapFileName == null || mapFileName.isEmpty()) {
      LOGGER.warning("Missing map file name!");
      response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing map file name!");
      return 0;
    }

    if ((mapFileName = ServerUtils.checkMapFileName(mapFileName)) == null) {
      LOGGER.warning("Invalid map file name: " + mapFileName);
      response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid map file name!");
      return 0;
    }

    try {
      final XmlBuilder xb = new XmlBuilder("1.0");

      long opsCharged = 0;

      final List<Map> mapList =
          new JQBuilder<>(pm, Map.class).filter("fname==p1", "String p1").get(mapFileName);
      if (mapList.isEmpty()) {
        // Register a task to process the map
        TaskServlet.register_processMapTask(mapFileName);
        xb.createResultElement(MapInfoResult.PROCESSING);
      } else {
        final Map map = mapList.get(0);
        switch (map.getStatus()) {
          case Map.STATUS_PROCESSING:
            xb.createResultElement(MapInfoResult.PROCESSING);
            break;
          case Map.STATUS_PARSING_ERROR:
            xb.createResultElement(MapInfoResult.PARSING_ERROR);
            break;
          case Map.STATUS_DL_ERROR:
            xb.createResultElement(MapInfoResult.DOWNLOAD_ERROR);
            break;
          case Map.STATUS_READY:
            {
              opsCharged = 1;
              xb.createResultElement(MapInfoResult.OK);
              Element element = xb.createElement(XTAG_MAP);
              element.setAttribute(XATTR_NAME, map.getName());
              element.setAttribute(XATTR_WIDTH, Integer.toString(map.getMwidth()));
              element.setAttribute(XATTR_HEIGHT, Integer.toString(map.getMheight()));
              element = xb.createElement(XTAG_MAP_IMAGE);
              element.setAttribute(XATTR_FORMAT, "JPEG");
              element.setAttribute(XATTR_SIZE, Integer.toString(map.getSize()));
              element.setAttribute(XATTR_WIDTH, Integer.toString(map.getWidth()));
              element.setAttribute(XATTR_HEIGHT, Integer.toString(map.getHeight()));
              element.setTextContent(
                  javax.xml.bind.DatatypeConverter.printBase64Binary(map.getImage().getBytes()));
              break;
            }
        }
      }

      xb.printDocument(response);

      return opsCharged;
    } catch (final Exception e) {
      LOGGER.log(Level.SEVERE, "", e);
      response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
      return 0;
    }
  }

  /** Parse replay operation. */
  private long parseRepOp(
      final HttpServletRequest request,
      final HttpServletResponse response,
      final PersistenceManager pm,
      final ApiAccount apiAccount)
      throws IOException {
    final String fileContent = request.getParameter(PARAM_FILE_CONTENT);
    final Integer fileLength = getIntParam(request, PARAM_FILE_LENGTH);
    LOGGER.fine("API account: " + apiAccount.getUser().getEmail() + ", file length: " + fileLength);
    if (fileContent == null || fileContent.isEmpty() || fileLength == null) {
      LOGGER.warning("Missing parameters!");
      response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing parameters!");
      return 0;
    }

    final byte[] decodedFileContent = ServerUtils.decodeBase64String(fileContent);
    if (decodedFileContent == null) {
      LOGGER.warning("Invalid Base64 encoded file content!");
      response.sendError(
          HttpServletResponse.SC_BAD_REQUEST, "Invalid Base64 encoded file content!");
      return 0;
    }
    if (decodedFileContent.length != fileLength) {
      LOGGER.warning(
          "Supplied file length does not match decoded file content length: "
              + fileLength
              + " != "
              + decodedFileContent.length);
      response.sendError(
          HttpServletResponse.SC_BAD_REQUEST,
          "Supplied file length does not match decoded file content length!");
      return 0;
    }

    try {
      long opsCharged = 1;

      final Boolean parseMessagesParam = getBooleanParam(request, PARAM_PARSE_MESSAGES);
      final Boolean parseActionsParam = getBooleanParam(request, PARAM_PARSE_ACTIONS);

      // Default or requested values:
      final boolean parseMessages = parseMessagesParam == null ? true : parseMessagesParam;
      final boolean parseActions = parseActionsParam == null ? true : parseActionsParam;

      final Set<ReplayContent> contentToExtractSet =
          EnumSet.copyOf(ReplayFactory.GENERAL_INFO_CONTENT);
      if (parseMessages) contentToExtractSet.add(ReplayContent.MESSAGE_EVENTS);
      if (parseActions) {
        contentToExtractSet.add(ReplayContent.GAME_EVENTS);
        opsCharged++;
      }

      final Replay replay =
          ReplayFactory.parseReplay(
              "attachedFile.SC2Replay",
              new MpqParser(new ByteArrayMpqDataInput(decodedFileContent)),
              contentToExtractSet);

      final XmlBuilder xb = new XmlBuilder("1.0");
      xb.createResultElement(replay == null ? ParseRepResult.PARSING_ERROR : ParseRepResult.OK);
      xb.createElement(XTAG_ENGINE_VER, ReplayFactory.getVersion());
      if (replay == null) {
        LOGGER.fine("Replay parsing error!");
        xb.printDocument(response);
        return opsCharged;
      }

      final Element repInfoElement = xb.setParentElement(xb.createElement(XTAG_REP_INFO));
      xb.createElement(XTAG_VERSION, replay.version);
      xb.createElement(XTAG_EXPANSION, replay.details.expansion);
      final Element gameLengthSecElement =
          xb.createElement(
              XTAG_GAME_LENGTH, replay.converterGameSpeed.convertToRealTime(replay.gameLengthSec));
      gameLengthSecElement.setAttribute(XATTR_UNIT, "sec");
      gameLengthSecElement.setAttribute(
          XATTR_GAME_TIME_VALUE, Integer.toString(replay.gameLengthSec));
      xb.createElement(XTAG_GAME_LENGTH, replay.frames).setAttribute(XATTR_UNIT, "frame");
      xb.createElement(XTAG_GAME_TYPE, replay.initData.gameType);
      if (replay.initData.competitive != null)
        xb.createElement(XTAG_IS_COMPETITIVE, replay.initData.competitive);
      xb.createElement(XTAG_GAME_SPEED, replay.initData.gameSpeed);
      xb.createElement(XTAG_FORMAT, replay.initData.format);
      xb.createElement(XTAG_GATEWAY, replay.initData.gateway);
      xb.createElement(XTAG_MAP_FILE_NAME, replay.initData.mapFileName);
      xb.setParentElement(xb.createElement(XTAG_CLIENTS));
      final Player[] players = replay.details.players;
      final String[] arrangedClientNames = replay.initData.getArrangedClientNames(players);
      xb.getParentElement().setAttribute(XATTR_COUNT, Integer.toString(arrangedClientNames.length));
      for (int i = 0; i < arrangedClientNames.length; i++)
        xb.createElement(XTAG_CLIENT, arrangedClientNames[i])
            .setAttribute(XATTR_INDEX, Integer.toString(i));
      xb.setParentElement(repInfoElement);
      xb.createElement(XTAG_MAP_NAME, replay.details.originalMapName);
      xb.createDateTimeElement(XTAG_SAVE_TIME, new Date(replay.details.saveTime));
      xb.createElement(
          XTAG_SAVE_TIME_ZONE, String.format(Locale.US, "%+.2f", replay.details.saveTimeZone));
      final Element playersElement = xb.createElement(XTAG_PLAYERS);
      xb.setParentElement(playersElement);
      playersElement.setAttribute(XATTR_COUNT, Integer.toString(players.length));
      for (int i = 0; i < players.length; i++) {
        final Player player = players[i];
        xb.setParentElement(playersElement);
        xb.setParentElement(xb.createElement(XTAG_PLAYER, XATTR_INDEX, Integer.toString(i)));
        final Element playerElement =
            xb.createElement(XTAG_PLAYER_ID, XATTR_NAME, player.playerId.name);
        playerElement.setAttribute(XATTR_BNET_ID, Integer.toString(player.playerId.battleNetId));
        playerElement.setAttribute(
            XATTR_BNET_SUBID, Integer.toString(player.playerId.battleNetSubId));
        playerElement.setAttribute(XATTR_GATEWAY, player.playerId.gateway.toString());
        playerElement.setAttribute(XATTR_GW_CODE, player.playerId.gateway.binaryValue);
        playerElement.setAttribute(XATTR_REGION, player.playerId.getRegion().toString());
        playerElement.setAttribute(
            XATTR_PROFILE_URL,
            player.playerId.getBattleNetProfileUrl(player.playerId.gateway.defaultLanguage));
        xb.createElement(
            XTAG_TEAM, player.team == Player.TEAM_UNKNOWN ? TEXT_UNKNOWN : player.team);
        xb.createElement(XTAG_RACE, player.race);
        xb.createElement(XTAG_FINAL_RACE, player.finalRace);
        xb.createElement(XTAG_LEAGUE, player.getLeague());
        xb.createElement(XTAG_SWARM_LEVELS, player.getSwarmLevels());
        final Element colorElement = xb.createElement(XTAG_COLOR, XATTR_NAME, player.playerColor);
        colorElement.setAttribute(XATTR_RED, Integer.toString(player.argbColor[1]));
        colorElement.setAttribute(XATTR_GREEN, Integer.toString(player.argbColor[2]));
        colorElement.setAttribute(XATTR_BLUE, Integer.toString(player.argbColor[3]));
        xb.createElement(XTAG_TYPE, player.type);
        xb.createElement(XTAG_DIFFICULTY, player.difficulty);
        xb.createElement(XTAG_HANDICAP, player.handicap);
        xb.createElement(XTAG_IS_WINNER, player.isWinner == null ? TEXT_UNKNOWN : player.isWinner);

        if (parseActions) {
          xb.createElement(XTAG_ACTIONS_COUNT, player.actionsCount);
          xb.createElement(XTAG_EFFECTIVE_ACTIONS_COUNT, player.effectiveActionsCount);
          xb.createElement(XTAG_LAST_ACTION_FRAME, player.lastActionFrame);
          xb.createElement(XTAG_APM, ReplayUtils.calculatePlayerApm(replay, player))
              .setAttribute(
                  XATTR_EXCLUDED_ACTIONS_COUNT, Integer.toString(player.excludedActionsCount));
          xb.createElement(XTAG_EAPM, ReplayUtils.calculatePlayerEapm(replay, player))
              .setAttribute(
                  XATTR_EXCLUDED_ACTIONS_COUNT,
                  Integer.toString(player.excludedEffectiveActionsCount));
          Float fvalue;
          xb.createElement(
                  XTAG_AVG_SPAWNING_RATIO,
                  (fvalue = player.getAverageSpawningRatio()) == null
                      ? TEXT_UNKNOWN
                      : (int) (fvalue * 100))
              .setAttribute(XATTR_UNIT, "%");
          xb.createElement(
                  XTAG_AVG_INJECTION_GAP,
                  (fvalue = player.getAverageInjectionGap()) == null
                      ? TEXT_UNKNOWN
                      : ReplayUtils.formatFramesDecimal(
                          fvalue.intValue(), replay.converterGameSpeed))
              .setAttribute(XATTR_UNIT, "sec");
        }
      }

      if (parseMessages) {
        xb.setParentElement(null); // Root element
        final Element inGameChatElement =
            xb.createElement(XTAG_IN_GAME_CHAT, XATTR_COUNT, replay.messageEvents.messages.length);
        inGameChatElement.setAttribute(XATTR_PATTERN, "HH:mm:ss");
        xb.setParentElement(inGameChatElement);
        int ms = 0;
        for (final Message message : replay.messageEvents.messages) {
          ms += message.time;
          final Element messageElement =
              xb.createElement(
                  message instanceof Text ? XTAG_TEXT : XTAG_PING,
                  XATTR_CLIENT_INDEX,
                  message.client);
          messageElement.setAttribute(XATTR_CLIENT, arrangedClientNames[message.client]);
          messageElement.setAttribute(
              XATTR_TIME, ReplayUtils.formatMs(ms, replay.converterGameSpeed));
          if (message instanceof Text) {
            messageElement.setAttribute(XATTR_VALUE, ((Text) message).text);
            final byte opCode = ((Text) message).opCode;
            messageElement.setAttribute(
                XATTR_TARGET,
                opCode == MessageEvents.OP_CODE_CHAT_TO_ALL
                    ? "all"
                    : opCode == MessageEvents.OP_CODE_CHAT_TO_ALLIES
                        ? "allies"
                        : opCode == MessageEvents.OP_CODE_CHAT_TO_OBSERVERS
                            ? "observers"
                            : "unknown");
          } else if (message instanceof Blink) {
            messageElement.setAttribute(XATTR_X, ReplayUtils.formatCoordinate(((Blink) message).x));
            messageElement.setAttribute(XATTR_Y, ReplayUtils.formatCoordinate(((Blink) message).y));
          }
        }
      }

      if (parseActions) {
        final Boolean sendActionsSelectParam = getBooleanParam(request, PARAM_SEND_ACTIONS_SELECT);
        final Boolean sendActionsBuildParam = getBooleanParam(request, PARAM_SEND_ACTIONS_BUILD);
        final Boolean sendActionsTrainParam = getBooleanParam(request, PARAM_SEND_ACTIONS_TRAIN);
        final Boolean sendActionsResearchParam =
            getBooleanParam(request, PARAM_SEND_ACTIONS_RESEARCH);
        final Boolean sendActionsUpgradeParam =
            getBooleanParam(request, PARAM_SEND_ACTIONS_UPGRADE);
        final Boolean sendActionsOtherParam = getBooleanParam(request, PARAM_SEND_ACTIONS_OTHER);
        final Boolean sendActionsInactionParam =
            getBooleanParam(request, PARAM_SEND_ACTIONS_INACTION);

        // Default or requested values:
        final boolean sendActionsSelect =
            sendActionsSelectParam == null ? false : sendActionsSelectParam;
        final boolean sendActionsBuild =
            sendActionsBuildParam == null ? true : sendActionsBuildParam;
        final boolean sendActionsTrain =
            sendActionsTrainParam == null ? true : sendActionsTrainParam;
        final boolean sendActionsResearch =
            sendActionsResearchParam == null ? true : sendActionsResearchParam;
        final boolean sendActionsUpgrade =
            sendActionsUpgradeParam == null ? true : sendActionsUpgradeParam;
        final boolean sendActionsOther =
            sendActionsOtherParam == null ? false : sendActionsOtherParam;
        final boolean sendActionsInaction =
            sendActionsInactionParam == null ? false : sendActionsInactionParam;

        final Set<ActionType> sendActionTypeSet = EnumSet.noneOf(ActionType.class);
        if (sendActionsSelect) sendActionTypeSet.add(ActionType.SELECT);
        if (sendActionsBuild) sendActionTypeSet.add(ActionType.BUILD);
        if (sendActionsTrain) sendActionTypeSet.add(ActionType.TRAIN);
        if (sendActionsResearch) sendActionTypeSet.add(ActionType.RESEARCH);
        if (sendActionsUpgrade) sendActionTypeSet.add(ActionType.UPGRADE);
        if (sendActionsOther) sendActionTypeSet.add(ActionType.OTHER);
        if (sendActionsInaction) sendActionTypeSet.add(ActionType.INACTION);

        if (sendActionsSelect || sendActionsOther) opsCharged++;
        if (sendActionsInaction) opsCharged++;

        xb.setParentElement(null); // Root element
        final Element actionsElement =
            xb.createElement(
                XTAG_ACTIONS, XATTR_ALL_ACTIONS_COUNT, replay.gameEvents.actions.length);
        actionsElement.setAttribute(
            XATTR_ERROR_PARSING, Boolean.toString(replay.gameEvents.errorParsing));
        xb.setParentElement(actionsElement);

        int count = 0; // Sent actions count
        if (!sendActionTypeSet.isEmpty()) {
          final StringBuilder actionStringBuilder = new StringBuilder();
          for (final Action action : replay.gameEvents.actions)
            if (sendActionTypeSet.contains(action.type)) {
              count++;
              final Element actionElement =
                  xb.createElement(XTAG_ACTION_, XATTR_PLAYER_, action.player);
              actionElement.setAttribute(XATTR_TYPE_, ACTION_TYPE_STRINGS[action.type.ordinal()]);
              actionElement.setAttribute(XATTR_FRAME_, Integer.toString(action.frame));
              actionStringBuilder.setLength(0);
              action.customToString(actionStringBuilder);
              actionElement.setAttribute(XATTR_STRING_, actionStringBuilder.toString());
            }
        }
        actionsElement.setAttribute(XATTR_COUNT, Integer.toString(count));
      }

      xb.printDocument(response);

      return opsCharged;
    } catch (final InvalidMpqArchiveException imae) {
      LOGGER.log(Level.WARNING, "", imae);
      response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Invalid SC2Replay file!");
      return 0;
    } catch (final Exception e) {
      LOGGER.log(Level.SEVERE, "", e);
      response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
      return 0;
    }
  }

  /** Profile info operation. */
  private long profInfoOp(
      final HttpServletRequest request,
      final HttpServletResponse response,
      final PersistenceManager pm,
      final ApiAccount apiAccount)
      throws IOException {
    LOGGER.fine("API account: " + apiAccount.getUser().getEmail());

    final Integer bnetId;
    final Integer bnetSubId;
    final String gatewayString;
    final String playerName;
    final Gateway gateway;

    final String bnetProfileUrlParam = request.getParameter(PARAM_BNET_PROFILE_URL);
    if (bnetProfileUrlParam == null || bnetProfileUrlParam.isEmpty()) {
      // Player id is provided explicitly
      bnetId = getIntParam(request, PARAM_BNET_ID);
      bnetSubId = getIntParam(request, PARAM_BNET_SUBID);
      gatewayString = request.getParameter(PARAM_GATEWAY);
      playerName = request.getParameter(PARAM_PLAYER_NAME);
      if (bnetId == null
          || bnetSubId == null
          || gatewayString == null
          || gatewayString.isEmpty()
          || playerName == null
          || playerName.isEmpty()) {
        LOGGER.warning("Missing parameters!");
        response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing parameters!");
        return 0;
      }
      gateway = Gateway.fromBinaryValue(gatewayString);
      if (gateway == Gateway.UNKNOWN || gateway == Gateway.PUBLIC_TEST) {
        LOGGER.warning("Invalid gateway parameter: " + gatewayString);
        response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid gateway parameter!");
        return 0;
      }
    } else {
      // Player id is provided through his/her Battle.net profile URL
      try {
        Gateway foundGateway = null;
        for (final Gateway gateway_ : EnumCache.GATEWAYS)
          if (bnetProfileUrlParam.startsWith(gateway_.bnetUrl)) {
            foundGateway = gateway_;
            break;
          }
        if (foundGateway == null
            || foundGateway == Gateway.UNKNOWN
            || foundGateway == Gateway.PUBLIC_TEST) throw new Exception("No matching gateway!");
        gateway = foundGateway;

        final String[] urlParts = bnetProfileUrlParam.split("/");
        if (urlParts.length < 3) throw new Exception("Not enough parts in URL!");

        playerName = URLDecoder.decode(urlParts[urlParts.length - 1], "UTF-8");
        bnetSubId = Integer.valueOf(urlParts[urlParts.length - 2]);
        bnetId = Integer.valueOf(urlParts[urlParts.length - 3]);

      } catch (final Exception e) {
        LOGGER.log(Level.SEVERE, "Invalid Battle.net profile URL: " + bnetProfileUrlParam, e);
        response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid Battle.net profile URL!");
        return 0;
      }
    }

    long opsCharged = 1;

    final Boolean retrieveExtInfoParam = getBooleanParam(request, PARAM_RETRIEVE_EXT_INFO);
    // Default or requested values:
    final boolean retrieveExtInfo = retrieveExtInfoParam == null ? false : retrieveExtInfoParam;

    try {
      final PlayerId playerId = new PlayerId();
      playerId.battleNetId = bnetId;
      playerId.battleNetSubId = bnetSubId;
      playerId.gateway = gateway;
      playerId.name = playerName;

      final String bnetProfileUrl = playerId.getBattleNetProfileUrl(BnetLanguage.ENGLISH);

      LOGGER.fine("Bnet profile URL: " + bnetProfileUrl);

      final URLFetchService urlFetchService = URLFetchServiceFactory.getURLFetchService();

      // Default deadline: 5 seconds... increase it!
      final HTTPRequest profileRequest = new HTTPRequest(new URL(bnetProfileUrl));
      profileRequest
          .getFetchOptions()
          .setDeadline(50.0); // 50-sec deadline (leaving 10 seconds to process...
      final Future<HTTPResponse> profileFetchAsync = urlFetchService.fetchAsync(profileRequest);

      final Future<HTTPResponse> extProfileFetchAsync;
      if (retrieveExtInfo) {
        opsCharged++;
        // Start retrieving extended profile info in parallel
        final HTTPRequest extProfileRequest =
            new HTTPRequest(new URL(bnetProfileUrl + "ladder/leagues"));
        extProfileRequest
            .getFetchOptions()
            .setDeadline(50.0); // 50-sec deadline (leaving 10 seconds to process...
        extProfileFetchAsync = urlFetchService.fetchAsync(extProfileRequest);
      } else extProfileFetchAsync = null;

      final XmlBuilder xb = new XmlBuilder("1.1");

      HTTPResponse profileResponse = null;
      Profile profile = null;
      Element profInfoElement = null;
      try {
        profileResponse = profileFetchAsync.get();

        switch (profileResponse.getResponseCode()) {
          case HttpServletResponse.SC_OK:
            {
              final byte[] content = profileResponse.getContent();
              if (content.length == 0) throw new Exception("Content length = 0!");
              profile = BnetUtils.retrieveProfile(null, new ByteArrayInputStream(content));
              if (profile != null) {
                LOGGER.fine("Parse OK");

                xb.createResultElement(ProfInfoResult.OK);
                opsCharged += 2;

                profInfoElement = xb.setParentElement(xb.createElement(XTAG_PROFILE_INFO));
                // Re-include player id
                final Element playerElement =
                    xb.createElement(XTAG_PLAYER_ID, XATTR_NAME, playerName);
                playerElement.setAttribute(XATTR_BNET_ID, bnetId.toString());
                playerElement.setAttribute(XATTR_BNET_SUBID, bnetSubId.toString());
                playerElement.setAttribute(XATTR_GATEWAY, gateway.toString());
                playerElement.setAttribute(XATTR_GW_CODE, gateway.binaryValue);
                playerElement.setAttribute(XATTR_REGION, playerId.getRegion().toString());
                playerElement.setAttribute(XATTR_PROFILE_URL, bnetProfileUrl);
                final Element portraitElement =
                    xb.createElement(XTAG_PORTRAIT, XATTR_GROUP, profile.portraitGroup);
                portraitElement.setAttribute(XATTR_ROW, Integer.toString(profile.portraitRow));
                portraitElement.setAttribute(
                    XATTR_COLUMN, Integer.toString(profile.portraitColumn));
                xb.createElement(XTAG_ACHIEVEMENT_POINTS, profile.achievementPoints);
                xb.createElement(XTAG_TOTAL_CAREER_GAMES, profile.totalCareerGames);
                xb.createElement(XTAG_GAMES_THIS_SEASON, profile.gamesThisSeason);
                xb.createElement(XTAG_TERRAN_WINS, profile.terranWins);
                xb.createElement(XTAG_ZERG_WINS, profile.zergWins);
                xb.createElement(XTAG_PROTOSS_WINS, profile.protossWins);
                final Element highestSoloFlElement =
                    xb.createElement(XTAG_HIGHEST_SOLO_FL, profile.highestSoloFinishLeague);
                if (profile.highestSoloFinishTimes > 0)
                  highestSoloFlElement.setAttribute(
                      XATTR_TIMES_ACHIEVED, Integer.toString(profile.highestSoloFinishTimes));
                final Element highestTeamFlElement =
                    xb.createElement(XTAG_HIGHEST_TEAM_FL, profile.highestTeamFinishLeague);
                if (profile.highestTeamFinishTimes > 0)
                  highestTeamFlElement.setAttribute(
                      XATTR_TIMES_ACHIEVED, Integer.toString(profile.highestTeamFinishTimes));

                break;
              } else {
                LOGGER.fine("Parse error!");
                xb.createResultElement(ProfInfoResult.PARSING_ERROR); // Parse fails
              }
            }
          case HttpServletResponse.SC_NOT_FOUND:
            LOGGER.fine("Invalid player!");
            xb.createResultElement(ProfInfoResult.INVALID_PLAYER);
            break;
          default:
            // Treat other response HTTP status codes as BNET_ERROR
            throw new Exception("Response code: " + profileResponse.getResponseCode());
        }

      } catch (final Exception e) {
        LOGGER.log(Level.SEVERE, "", e);
        xb.createResultElement(ProfInfoResult.BNET_ERROR);
      } finally {
        if (retrieveExtInfo && profile == null) extProfileFetchAsync.cancel(true);
      }

      if (retrieveExtInfo && profile != null) {
        try {
          profileResponse = extProfileFetchAsync.get();

          final byte[] content;
          if (profileResponse.getResponseCode() == HttpServletResponse.SC_OK
              && (content = profileResponse.getContent()).length > 0) {
            profile =
                BnetUtils.retrieveExtProfile(null, new ByteArrayInputStream(content), profile);
            if (profile != null) {
              LOGGER.fine("Parse extended OK");

              opsCharged += 2;
              xb.setParentElement(profInfoElement);
              final Element allRankGroupsElement =
                  xb.setParentElement(xb.createElement(XTAG_ALL_RANK_GROUPS));
              int allRankGroupsCount = 0;
              for (int bracket = 0; bracket < profile.allRankss.length; bracket++) {
                final TeamRank[] allRanks = profile.allRankss[bracket];
                if (allRanks != null && allRanks.length > 0) {
                  allRankGroupsCount++;
                  xb.setParentElement(allRankGroupsElement);
                  final Element allRankGroupElement =
                      xb.createElement(XTAG_ALL_RANK_GROUP, XATTR_COUNT, allRanks.length);
                  allRankGroupElement.setAttribute(
                      XATTR_FORMAT, (bracket + 1) + "v" + (bracket + 1));

                  for (int i = 0; i < allRanks.length; i++) {
                    xb.setParentElement(allRankGroupElement);
                    final Element teamRankElement =
                        xb.setParentElement(
                            xb.createElement(
                                XTAG_TEAM_RANK, XATTR_LEAGUE, allRanks[i].league.stringValue));
                    teamRankElement.setAttribute(
                        XATTR_DIVISION_RANK, Integer.toString(allRanks[i].divisionRank));
                    // Team members
                    xb.setParentElement(
                        xb.createElement(
                            XTAG_TEAM_MEMBERS, XATTR_COUNT, allRanks[i].teamMembers.length));
                    for (final String memberName : allRanks[i].teamMembers)
                      xb.createElement(XTAG_TEAM_MEMBER, XATTR_NAME, memberName);
                  }
                }
              }
              allRankGroupsElement.setAttribute(XATTR_COUNT, Integer.toString(allRankGroupsCount));
            } else LOGGER.fine("Parse extended error!");
          }
        } catch (final Exception e) {
          LOGGER.log(
              Level.SEVERE,
              "Failed to get extended profile info, we return the basic profile info silently.",
              e);
          // Failed to get extended profile info, we return the basic profile info silently
        }
      }

      xb.printDocument(response);

      return opsCharged;

    } catch (final Exception e) {
      LOGGER.log(Level.SEVERE, "", e);
      response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
      return 0;
    }
  }
}