/*
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2012 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* muCommander is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.ui.dialog.file;
import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.file.util.FileSet;
import com.mucommander.commons.file.util.PathUtils;
import com.mucommander.commons.util.StringUtils;
import com.mucommander.job.BatchRenameJob;
import com.mucommander.text.Translator;
import com.mucommander.ui.action.ActionProperties;
import com.mucommander.ui.action.impl.BatchRenameAction;
import com.mucommander.ui.dialog.FocusDialog;
import com.mucommander.ui.layout.XAlignedComponentPanel;
import com.mucommander.ui.layout.XBoxPanel;
import com.mucommander.ui.layout.YBoxPanel;
import com.mucommander.ui.main.MainFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.TableModelEvent;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableColumn;
import javax.swing.text.BadLocationException;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.text.NumberFormat;
import java.util.*;
/**
* Dialog used to set parameters for renaming multiple files.
*
* @author Mariusz Jakubowski
*/
public class BatchRenameDialog extends FocusDialog implements ActionListener, DocumentListener {
private static final Logger LOGGER = LoggerFactory.getLogger(BatchRenameDialog.class);
private static final int CASE_UNCHANGED = 0;
private static final int CASE_LOWER = 1;
private static final int CASE_UPPER = 2;
private static final int CASE_FIRST_UPPER = 3;
private static final int CASE_WORD_UPPER = 4;
private static final int COL_ORIG_NAME = 0;
private static final int COL_CHANGED_NAME = 1;
private static final int COL_CHANGE_BLOCK = 2;
private MainFrame mainFrame;
private JTextField edtFileNameMask;
private JTable tblNames;
private JButton btnRename;
private JButton btnClose;
private JTextField edtSearchFor;
private JTextField edtReplaceWith;
private JTextField edtCounterStart;
private JTextField edtCounterStep;
private JComboBox cbCounterDigits;
private JComboBox cbCase;
private RenameTableModel tableModel;
private AbstractAction actRemove;
private JButton btnName;
private JButton btnNameRange;
private JButton btnExtension;
private JButton btnCounter;
private JLabel lblDuplicates;
private TableColumn colBlock;
/** files to rename */
private FileSet files;
/** a map of old file names used to check for name conflicts */
private HashMap<String, AbstractFile> oldNames = new HashMap<String, AbstractFile>();
/** a list of generated names */
private List<String> newNames = new ArrayList<String>();
/** a list of flags to block file rename */
private List<Boolean> blockNames = new ArrayList<Boolean>();
/** a list of parsed tokens */
private List<AbstractToken> tokens = new ArrayList<AbstractToken>();
/**
* Creates a new batch-rename dialog.
* @param mainFrame the main frame
* @param files a list of files to rename
*/
public BatchRenameDialog(MainFrame mainFrame, FileSet files) {
super(mainFrame, ActionProperties.getActionLabel(BatchRenameAction.Descriptor.ACTION_ID), null);
this.mainFrame = mainFrame;
this.files = files;
for (AbstractFile f : files) {
this.blockNames.add(Boolean.FALSE);
this.newNames.add("");
oldNames.put(PathUtils.removeTrailingSeparator(f.getAbsolutePath()), f);
}
initialize();
generateNewNames();
}
/**
* Initializes the dialog.
*/
private void initialize() {
Container content = getContentPane();
content.setLayout(new BorderLayout());
content.add(getPnlTop(), BorderLayout.NORTH);
content.add(new JScrollPane(getTblNames()), BorderLayout.CENTER);
content.add(getPnlButtons(), BorderLayout.SOUTH);
getRootPane().setDefaultButton(btnRename);
}
/**
* Creates bottom panel with buttons.
*/
private JPanel getPnlButtons() {
JPanel pnlButtons = new JPanel(new BorderLayout());
pnlButtons.add(new JButton(getActRemove()), BorderLayout.WEST);
XBoxPanel pnlButtonsRight = new XBoxPanel();
lblDuplicates = new JLabel(Translator.get("batch_rename_dialog.duplicate_names"));
lblDuplicates.setForeground(Color.red);
lblDuplicates.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 15));
pnlButtonsRight.add(lblDuplicates);
btnRename = new JButton(Translator.get("rename"));
btnRename.addActionListener(this);
pnlButtonsRight.add(btnRename);
btnClose = new JButton(Translator.get("cancel"));
btnClose.addActionListener(this);
pnlButtonsRight.add(btnClose);
pnlButtons.add(pnlButtonsRight, BorderLayout.EAST);
return pnlButtons;
}
/**
* Creates a table with file names.
*/
private JTable getTblNames() {
if (tblNames == null) {
tableModel = new RenameTableModel();
tblNames = new JTable(tableModel);
// add del key for remove action
tblNames.getActionMap().put("del", getActRemove());
tblNames.getInputMap().put((KeyStroke) getActRemove().getValue(Action.ACCELERATOR_KEY), "del");
// add tab key
tblNames.getActionMap().put("tab", new AbstractAction() {
public void actionPerformed(ActionEvent e) {
tblNames.transferFocus();
}
});
tblNames.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0, false), "tab");
// add shift tab key
tblNames.getActionMap().put("shift tab", new AbstractAction() {
public void actionPerformed(ActionEvent e) {
tblNames.transferFocusBackward();
}
});
tblNames.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB,
InputEvent.SHIFT_DOWN_MASK, false), "shift tab");
//
tblNames.getColumnModel().getColumn(COL_CHANGED_NAME).setCellEditor(new
DefaultCellEditor(new JTextField()));
colBlock = tblNames.getColumnModel().getColumn(COL_CHANGE_BLOCK);
colBlock.setMaxWidth(60);
tblNames.removeColumn(colBlock);
}
return tblNames;
}
public Action getActRemove() {
if (actRemove == null) {
actRemove = new AbstractAction() {
public void actionPerformed(ActionEvent e) {
removeSelectedFiles();
}
};
actRemove.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0));
actRemove.putValue(Action.NAME, Translator.get("remove"));
}
return actRemove;
}
/**
* Creates a panel with edit controls.
*/
private JPanel getPnlTop() {
// file & extension mask
edtFileNameMask = new JTextField("[N].[E]");
edtFileNameMask.setColumns(20);
edtFileNameMask.getDocument().addDocumentListener(this);
edtFileNameMask.setToolTipText(getPatternHelp());
// search & replace
edtSearchFor = new JTextField();
edtSearchFor.setColumns(20);
edtSearchFor.getDocument().addDocumentListener(this);
edtReplaceWith = new JTextField();
edtReplaceWith.setColumns(20);
edtReplaceWith.getDocument().addDocumentListener(this);
// upper/lower case
Vector<String> ulcase = new Vector<String>();
ulcase.add(Translator.get("batch_rename_dialog.no_change"));
ulcase.add(Translator.get("batch_rename_dialog.lower_case"));
ulcase.add(Translator.get("batch_rename_dialog.upper_case"));
ulcase.add(Translator.get("batch_rename_dialog.first_upper"));
ulcase.add(Translator.get("batch_rename_dialog.word"));
cbCase = new JComboBox(ulcase);
cbCase.addActionListener(this);
// counter
edtCounterStart = new JTextField("1");
edtCounterStart.getDocument().addDocumentListener(this);
edtCounterStart.setColumns(2);
edtCounterStep = new JTextField("1");
edtCounterStep.getDocument().addDocumentListener(this);
edtCounterStep.setColumns(2);
Vector<String> digits = new Vector<String>();
String zeros = "0000";
for (int i = 1; i <= 5; i++) {
digits.add(zeros.substring(0, i - 1) + "1");
}
cbCounterDigits = new JComboBox(digits);
cbCounterDigits.addActionListener(this);
// add controls
XBoxPanel pnlTop = new XBoxPanel();
YBoxPanel pnl1 = new YBoxPanel();
pnl1.setBorder(BorderFactory.createTitledBorder(
Translator.get("batch_rename_dialog.mask")));
pnl1.add(edtFileNameMask);
JPanel pnl1Btns = new JPanel(new GridLayout(3, 2));
btnName = new JButton("[N] - " + Translator.get("name"));
btnName.addActionListener(this);
btnName.setHorizontalAlignment(SwingConstants.LEFT);
pnl1Btns.add(btnName);
btnExtension = new JButton("[E] - " + Translator.get("extension"));
btnExtension.addActionListener(this);
btnExtension.setHorizontalAlignment(SwingConstants.LEFT);
pnl1Btns.add(btnExtension);
btnNameRange = new JButton("[N#-#] - " + Translator.get("batch_rename_dialog.range"));
btnNameRange.addActionListener(this);
btnNameRange.setHorizontalAlignment(SwingConstants.LEFT);
pnl1Btns.add(btnNameRange);
btnCounter = new JButton("[C] - " + Translator.get("batch_rename_dialog.counter"));
btnCounter.addActionListener(this);
btnCounter.setHorizontalAlignment(SwingConstants.LEFT);
pnl1Btns.add(btnCounter);
pnl1.add(pnl1Btns);
pnl1.add(new JPanel());
pnlTop.add(pnl1);
XAlignedComponentPanel pnl2 = new XAlignedComponentPanel(5);
pnl2.setBorder(BorderFactory.createTitledBorder(Translator
.get("batch_rename_dialog.search_replace")));
pnl2.addRow(Translator.get("batch_rename_dialog.search_for"),
edtSearchFor, 5);
pnl2.addRow(Translator.get("batch_rename_dialog.replace_with"),
edtReplaceWith, 5);
pnl2.addRow(Translator.get("batch_rename_dialog.upper_lower_case"),
cbCase, 5);
pnlTop.add(pnl2);
XAlignedComponentPanel pnl3 = new XAlignedComponentPanel(5);
pnl3.setBorder(BorderFactory.createTitledBorder(Translator
.get("batch_rename_dialog.counter") + " [C]"));
pnl3.addRow(Translator.get("batch_rename_dialog.start_at"),
edtCounterStart, 5);
pnl3.addRow(Translator.get("batch_rename_dialog.step_by"),
edtCounterStep, 5);
pnl3.addRow(Translator.get("batch_rename_dialog.format"),
cbCounterDigits, 5);
pnlTop.add(pnl3);
return pnlTop;
}
/**
* Creates a label with help.
* @return a label with help
*/
private String getPatternHelp() {
return "<html>" +
"[N] - the whole name<br>" +
"[N2,3] - 3 characters starting from the 2nd character of the name<br>" +
"[N2-5] - characters 2 to 5<br>" +
"[N2-] - all characters starting from the 2nd character<br>" +
"[N-3,2] - two characters starting at 3rd character from the end of the name<br>" +
"[N2--2] - characters from the 2nd to the 2nd-last character<br>" +
"[C] - inserts a counter<br>" +
"[C10,2,3] - inserts a counter starting at 10, steps by 2, uses 3 digits<br>" +
"[YMD] - inserts a year, month and day when the file was last modified"; // TODO add to dictionary
}
/**
* Removes selected files from a list of files to rename.
*/
private void removeSelectedFiles() {
int[] sel = tblNames.getSelectedRows();
for (int i = sel.length - 1; i >= 0; i--) {
files.remove(sel[i]);
newNames.remove(sel[i]);
blockNames.remove(sel[i]);
tableModel.fireTableRowsDeleted(sel[i], sel[i]);
}
if (files.size() == 0) {
dispose();
}
}
/**
* Checks if there are duplicates in new file names.
*/
private void checkForDuplicates() {
boolean duplicates = false;
boolean oldNamesConflict = false;
Set<String> names = new HashSet<String>();
for (int i=0; i<newNames.size(); i++) {
String newName = newNames.get(i);
AbstractFile file = files.get(i);
AbstractFile parent = file.getParent();
if (parent != null) {
newName = parent.getAbsolutePath(true) + newName;
}
if (names.contains(newName)) {
duplicates = true;
break;
}
AbstractFile oldFile = oldNames.get(newName);
if (oldFile!=null && oldFile!=file) {
oldNamesConflict = true;
break;
}
names.add(newName);
}
if (duplicates) {
lblDuplicates.setText(Translator.get("batch_rename_dialog.duplicate_names"));
}
if (oldNamesConflict) {
lblDuplicates.setText(Translator.get("batch_rename_dialog.names_conflict"));
}
lblDuplicates.setVisible(duplicates || oldNamesConflict);
btnRename.setEnabled(!duplicates && !oldNamesConflict);
}
/**
* Generate a new name for a file.
*
* @param file a file to change name to
* @return the new file name
*/
private String generateNewName(AbstractFile file) {
// apply pattern
String newName = applyPattern(file);
// search & replace
if (edtSearchFor.getText().length() > 0) {
newName = newName.replace(edtSearchFor.getText(), edtReplaceWith
.getText());
}
// remove trailing dot
if (newName.endsWith(".")) {
newName = newName.substring(0, newName.length() - 1);
}
// uppercase/lowercase
newName = changeCase(newName, cbCase.getSelectedIndex());
return newName;
}
/**
* Generates new names for all files.
*/
private void generateNewNames() {
compilePattern(edtFileNameMask.getText());
for (int i = 0; i < files.size(); i++) {
if (Boolean.FALSE.equals(blockNames.get(i))) {
AbstractFile file = files.get(i);
String newName = generateNewName(file);
newNames.set(i, newName);
}
}
checkForDuplicates();
tableModel.fireTableChanged(new TableModelEvent(tableModel, 0,
newNames.size(), 1, TableModelEvent.UPDATE));
}
/**
* Parses a pattern for a filename and it's extension and stores it in a
* list. A pattern is a combination of file and extension masks that a user
* enters in fields. These masks can contain special placeholders for
* previous name, part of it, counter, date, and others. These placeholders
* (or 'tokens') are always in brackets [ and ]. A part of pattern which is
* not in brackets is copied to a new name. This metod analyzes a pattern
* and stores token handlers responsible for substituting these placeholders
* for actual parts of a new file name.
*
* @see AbstractToken
* @param pattern a pattern for changing a file name and it's extension
*/
private void compilePattern(String pattern) {
tokens.clear();
for (int i = 0; i < pattern.length(); i++) {
char c = pattern.charAt(i);
if (c == '[') {
int tokenEnd = pattern.indexOf(']', i);
if (tokenEnd == -1) {
tokens.add(new CopyChar(pattern.substring(i)));
break;
}
String strToken = pattern.substring(i + 1, tokenEnd);
if (strToken.length() > 0) {
c = strToken.charAt(0);
AbstractToken t;
switch (c) {
case 'N':
t = new NameToken(strToken);
break;
case 'E':
t = new ExtToken(strToken);
break;
case 'C':
int start = StringUtils.parseIntDef(edtCounterStart
.getText(), 0);
int step = StringUtils.parseIntDef(edtCounterStep
.getText(), 0);
int digits = cbCounterDigits.getSelectedIndex() + 1;
t = new CounterToken(strToken, start, step, digits);
break;
case 'P':
t = new ParentDirToken(strToken);
break;
case 'Y':
case 'M':
case 'D':
case 'h':
case 'm':
case 's':
t = new DateToken(strToken);
break;
case '[':
t = new CopyChar("[");
break;
default:
t = new CopyChar("[" + strToken + "]");
break;
}
t.parse();
tokens.add(t);
}
i = tokenEnd;
} else {
tokens.add(new CopyChar(Character.toString(c)));
}
}
}
/**
* Changes case of a file name.
* @param oldName a name of file to change case
* @param newCase a type of change
* @return the name with changed case
*/
private String changeCase(String oldName, int newCase) {
String newName = "";
switch (newCase) {
case CASE_UNCHANGED:
newName = oldName;
break;
case CASE_LOWER:
newName = oldName.toLowerCase();
break;
case CASE_UPPER:
newName = oldName.toUpperCase();
break;
case CASE_FIRST_UPPER:
newName = oldName.substring(0, 1).toUpperCase()
+ oldName.substring(1).toLowerCase();
break;
case CASE_WORD_UPPER:
boolean afterSpace = true;
StringBuilder newNameCase = new StringBuilder();
for (int i = 0; i < oldName.length(); i++) {
if (oldName.charAt(i) == ' ') {
newNameCase.append(' ');
afterSpace = true;
} else {
if (afterSpace) {
newNameCase.append(Character.toUpperCase(oldName
.charAt(i)));
afterSpace = false;
} else {
newNameCase.append(oldName.charAt(i));
}
}
}
newName = newNameCase.toString();
break;
}
return newName;
}
/**
* Applies a compiled pattern to a file name and it's extension.
* @param file a file
* @return the new file name after applying a pattern
*/
private String applyPattern(AbstractFile file) {
StringBuilder filename = new StringBuilder();
for (AbstractToken token: tokens)
filename.append(token.apply(file));
return filename.toString();
}
/**
* Counts or removes unchanged files from change set.
* @param countOnly if true only counts files that are changing.
* @return number of changed files
*/
private int removeUnchangedFiles(boolean countOnly) {
// remove non changed files
Iterator<AbstractFile> fi = files.iterator();
Iterator<String> ni = newNames.iterator();
int changed = 0;
while (fi.hasNext()) {
AbstractFile file = fi.next();
String nn = ni.next();
if (file.getName().equals(nn)) {
if (!countOnly) {
fi.remove();
ni.remove();
}
} else {
changed++;
}
}
return changed;
}
/**
* Renames files.
*/
private void doRename() {
removeUnchangedFiles(false);
// start rename job
if (files.size() > 0) {
ProgressDialog progressDialog = new ProgressDialog(mainFrame,
Translator.get("progress_dialog.processing_files"));
BatchRenameJob job = new BatchRenameJob(progressDialog, mainFrame,
files, newNames);
progressDialog.start(job);
}
}
/**
* Inserts pattern into pattern edit field.
* @param pattern a text to insert
*/
private void insertPattern(String pattern) {
int pos = edtFileNameMask.getSelectionStart();
try {
int selLen = edtFileNameMask.getSelectionEnd() - edtFileNameMask.getSelectionStart();
if (selLen > 0) {
edtFileNameMask.getDocument().remove(edtFileNameMask.getSelectionStart(), selLen);
}
edtFileNameMask.getDocument().insertString(pos, pattern, null);
} catch (BadLocationException e) {
LOGGER.debug("Caught exception", e);
}
}
// /////////////////////////////////
// ActionListener implementation //
// /////////////////////////////////
public void actionPerformed(ActionEvent e) {
Object source = e.getSource();
if (source == btnClose) {
dispose();
} else if (source == btnRename) {
int unchanged = files.size();
int changed = removeUnchangedFiles(true);
if (changed > 0) {
BatchRenameConfirmationDialog dlg = new BatchRenameConfirmationDialog(mainFrame, files, changed, unchanged);
if (dlg.isProceedWithRename()) {
dispose();
doRename();
}
}
} else if (source == cbCase) {
generateNewNames();
} else if (source == cbCounterDigits) {
generateNewNames();
} else if (source == btnName) {
insertPattern("[N]");
} else if (source == btnExtension) {
insertPattern("[E]");
} else if (source == btnCounter) {
insertPattern("[C]");
} else if (source == btnNameRange) {
String firstFile = files.get(0).getNameWithoutExtension();
BatchRenameSelectRange dlg = new BatchRenameSelectRange(this, firstFile);
dlg.showDialog();
String range = dlg.getRange();
if (range != null) {
insertPattern(range);
}
}
}
// these methods are invoked when one of edit boxes changes
public void changedUpdate(DocumentEvent e) {
generateNewNames();
}
public void insertUpdate(DocumentEvent e) {
generateNewNames();
}
public void removeUpdate(DocumentEvent e) {
generateNewNames();
}
/**
* Table model with old and new file names.
* @author Mariusz Jakubowski
*
*/
private class RenameTableModel extends AbstractTableModel {
public int getColumnCount() {
return 3;
}
public int getRowCount() {
return files.size();
}
public Object getValueAt(int rowIndex, int columnIndex) {
AbstractFile f = files.get(rowIndex);
switch (columnIndex) {
case COL_ORIG_NAME:
return f.getName();
case COL_CHANGED_NAME:
return newNames.get(rowIndex);
case COL_CHANGE_BLOCK:
return blockNames.get(rowIndex);
}
return null;
}
/**
* Sets the value in the cell at columnIndex and rowIndex to aValue.
* Called when a user manually entered a new file name or blocked
* a name from a rename pattern.
*/
@Override
public void setValueAt(Object value, int rowIndex, int columnIndex) {
switch (columnIndex) {
case COL_CHANGED_NAME:
if (!newNames.get(rowIndex).equals(value)) {
newNames.set(rowIndex, (String)value);
if (Boolean.FALSE.equals(blockNames.get(rowIndex))) {
blockNames.set(rowIndex, Boolean.TRUE);
if (tblNames.getColumnCount() == 2) {
tblNames.addColumn(colBlock);
}
fireTableCellUpdated(rowIndex, COL_CHANGE_BLOCK);
}
checkForDuplicates();
}
break;
case COL_CHANGE_BLOCK:
blockNames.set(rowIndex, (Boolean)value);
if (Boolean.FALSE.equals(value)) {
AbstractFile file = files.get(rowIndex);
String newName = generateNewName(file);
newNames.set(rowIndex, newName);
fireTableCellUpdated(rowIndex, COL_CHANGED_NAME);
}
checkForDuplicates();
break;
}
}
@Override
public String getColumnName(int column) {
switch (column) {
case COL_ORIG_NAME:
return Translator.get("batch_rename_dialog.old_name");
case COL_CHANGED_NAME:
return Translator.get("batch_rename_dialog.new_name");
case COL_CHANGE_BLOCK:
return Translator.get("batch_rename_dialog.block_name");
}
return "";
}
@Override
public Class<?> getColumnClass(int columnIndex) {
if (columnIndex == COL_CHANGE_BLOCK) {
return Boolean.class;
} else {
return String.class;
}
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
return columnIndex != COL_ORIG_NAME;
}
}
/**
* Base class for handling tokens.
*
* @author Mariusz Jakubowski
*
*/
private abstract static class AbstractToken {
/** a string with a token */
protected String token;
/** a current position in the token */
protected int pos = 1;
/** a length of the token */
protected int len;
public AbstractToken(String token) {
this.token = token;
this.len = token.length();
}
/**
* Parses a token information.
*/
protected abstract void parse();
/**
* Applies this token to a file.
*
* @param file a file
* @return a part of filename after applying this token
*/
public abstract String apply(AbstractFile file);
/**
* Gets one char from this token.
*
* @return the next character in the token
*/
public char getChar() {
if (pos < len) {
return token.charAt(pos++);
}
return 0;
}
/**
* Trys to get an integer from this token string. Advances the current
* position in the token string.
*
* @param def
* a default value if an integer cannot be parsed
* @return the integer from this token string or the default value
*/
public int getInt(int def) {
int startpos = pos;
while (pos < len) {
char c = token.charAt(pos);
if (c < '0' || c > '9') {
if (c != '-' || startpos != pos)
break;
}
pos++;
}
if (startpos == pos)
return def;
return StringUtils.parseIntDef(token.substring(startpos, pos), def);
}
}
/**
* Token handler that copies a character from a source string to a
* destination. This is used for all characters without brackets.
*
* @author Mariusz Jakubowski
*
*/
static class CopyChar extends AbstractToken {
public CopyChar(String token) {
super(token);
}
@Override
protected void parse() {
}
@Override
public String apply(AbstractFile file) {
return token;
}
}
/**
* Token handler that parses file name. Examples:
* <ul>
* <li>[N] - whole name
* <li>[N2] - 2nd character of a name
* <li>[N2,3] - 3 characters starting at the 2nd character of a name
* <li>[N2-5] - characters 2 to 5
* <li>[N2-] - all characters starting from the 2nd character
* <li>[N-2] - 2nd character from the end of name
* <li>[N-3,2] - two characters starting at 3rd character from the end of a name
* <li>[N2--2] - characters from the 2nd to the 2nd-last character
* <li>[N-5-10] - characters from 5th from end to 10th from beginning of a name
* </ul>
*
* @author Mariusz Jakubowski
*
*/
static class NameToken extends AbstractToken {
private int startIndex;
private int endIndex;
private int charCount;
public NameToken(String token) {
super(token);
}
@Override
protected void parse() {
startIndex = getInt(0);
char sep = getChar();
switch (sep) {
case '-':
endIndex = getInt(999); // default - to the end
break;
case ',':
charCount = getInt(0);
}
}
@Override
public String apply(AbstractFile file) {
// split name & extension
String name;
String oldName = file.getName();
int dot = oldName.lastIndexOf('.');
if (dot >= 0) {
name = oldName.substring(0, dot);
} else {
name = oldName;
}
return extractNamePart(name);
}
/**
* Extracts a part of a name.
*
* @param name
* a string from which extract a part
* @return the part of the name
*/
protected String extractNamePart(String name) {
int targetLen = name.length();
int currentStartIndex = startIndex;
int currentEndIndex = endIndex;
if (currentStartIndex < 0) {
currentStartIndex = targetLen + currentStartIndex + 1;
if (currentStartIndex < 1)
currentStartIndex = 1;
}
if (currentEndIndex < 0)
currentEndIndex = targetLen + currentEndIndex + 1;
if (currentStartIndex > 0) {
if (charCount > 0) {
currentEndIndex = currentStartIndex + charCount - 1;
} else if (currentEndIndex == 0) {
currentEndIndex = currentStartIndex;
}
if (currentStartIndex <= currentEndIndex
&& currentStartIndex - 1 < targetLen) {
try {
name = name.substring(currentStartIndex - 1, Math.min(
currentEndIndex, targetLen));
} catch (Exception e) {
LOGGER.info("currentStartIndex="+currentStartIndex+", currentEndIndex="+currentEndIndex, e);
}
} else {
name = "";
}
}
return name;
}
}
/**
* Token handler that parses a file extension. [E] - an extension of a file,
* this token can also be used with parameters like in [N...]
*
* @author Mariusz Jakubowski
*/
static class ExtToken extends NameToken {
public ExtToken(String token) {
super(token);
}
@Override
public String apply(AbstractFile file) {
// split name & extension
String ext;
String oldName = file.getName();
int dot = oldName.lastIndexOf('.');
if (dot >= 0) {
ext = oldName.substring(dot + 1);
} else {
ext = "";
}
return extractNamePart(ext);
}
}
/**
* Token handler that inserts a counter.
* Examples:
* <ul>
* <li>[C] - inserts counter, as defined on the dialog
* <li>[C10] - inserts counter starting at 10
* <li>[C10,2] - inserts counter starting at 10, step by 2
* <li>[C10,-2] - inserts counter starting at 10, step by -2
* <li>[C10,2,3] - inserts counter starting at 10, step by 2, use 3 digits to display
* <li>[C10,,3] - inserts counter starting at 10, step by as defined on the dialog, use 3 digits to display
* </ul>
* @author Mariusz Jakubowski
*
*/
static class CounterToken extends AbstractToken {
private int start;
private int step;
private int digits;
private int current;
private NumberFormat numberFormat;
public CounterToken(String token, int start, int step, int digits) {
super(token);
this.start = start;
this.step = step;
this.digits = digits;
}
@Override
protected void parse() {
start = getInt(start);
if (getChar() == ',') {
step = getInt(step);
if (getChar() == ',') {
digits = getInt(digits);
}
}
numberFormat = NumberFormat.getIntegerInstance();
numberFormat.setMinimumIntegerDigits(digits);
numberFormat.setGroupingUsed(false);
current = start;
}
@Override
public String apply(AbstractFile file) {
String counter = numberFormat.format(current);
current += step;
return counter;
}
}
/**
* Token handler that inserts a directory information.
* [P] - inserts a name of the parent directory.
* @author Mariusz Jakubowski
*/
static class ParentDirToken extends NameToken {
public ParentDirToken(String token) {
super(token);
}
@Override
public String apply(AbstractFile file) {
AbstractFile parent = file.getParent();
if (parent != null)
return extractNamePart(parent.getName());
return "";
}
}
/**
* Inserts a date or time.
* <ul>
* <li>[Y] - inserts year (4 digits)
* <li>[M] - inserts month (2 digits)
* <li>[D] - inserts day of a month (2 digits)
* <li>[h] - inserts hours in 24-hour format (2 digits)
* <li>[m] - inserts minutes (2 digits)
* <li>[s] - inserts seconds
* <li>[YMD] - inserts file last modified year, month and day
* </ul>
* @author Mariusz Jakubowski
*
*/
static class DateToken extends AbstractToken {
private NumberFormat year;
private NumberFormat digits2;
public DateToken(String token) {
super(token);
year = NumberFormat.getIntegerInstance();
year.setMinimumIntegerDigits(4);
year.setGroupingUsed(false);
digits2 = NumberFormat.getIntegerInstance();
digits2.setMinimumIntegerDigits(2);
digits2.setGroupingUsed(false);
}
@Override
public String apply(AbstractFile file) {
Calendar c = Calendar.getInstance();
c.setTimeInMillis(file.getDate());
StringBuilder result = new StringBuilder();
for (int i = 0; i < len; i++) {
switch (token.charAt(i)) {
case 'Y':
result.append(year.format(c.get(Calendar.YEAR)));
break;
case 'M':
result.append(digits2.format(c.get(Calendar.MONTH)));
break;
case 'D':
result.append(digits2.format(c.get(Calendar.DAY_OF_MONTH)));
break;
case 'h':
result.append(digits2.format(c.get(Calendar.HOUR_OF_DAY)));
break;
case 'm':
result.append(digits2.format(c.get(Calendar.MINUTE)));
break;
case 's':
result.append(digits2.format(c.get(Calendar.SECOND)));
break;
}
}
return result.toString();
}
@Override
protected void parse() {
}
}
}