/* * Copyright 2009-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.codehaus.groovy.eclipse.refactoring.actions; import static org.eclipse.jdt.groovy.core.util.GroovyUtils.getBaseType; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import groovy.transform.Field; import org.codehaus.groovy.ast.AnnotationNode; import org.codehaus.groovy.ast.ClassCodeVisitorSupport; import org.codehaus.groovy.ast.ClassHelper; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.ConstructorNode; import org.codehaus.groovy.ast.DynamicVariable; import org.codehaus.groovy.ast.FieldNode; import org.codehaus.groovy.ast.GenericsType; import org.codehaus.groovy.ast.GroovyClassVisitor; import org.codehaus.groovy.ast.ImportNode; import org.codehaus.groovy.ast.MethodNode; import org.codehaus.groovy.ast.Parameter; import org.codehaus.groovy.ast.expr.AnnotationConstantExpression; import org.codehaus.groovy.ast.expr.ArrayExpression; import org.codehaus.groovy.ast.expr.CastExpression; import org.codehaus.groovy.ast.expr.ClassExpression; import org.codehaus.groovy.ast.expr.ClosureExpression; import org.codehaus.groovy.ast.expr.ClosureListExpression; import org.codehaus.groovy.ast.expr.ConstantExpression; import org.codehaus.groovy.ast.expr.ConstructorCallExpression; import org.codehaus.groovy.ast.expr.MethodCallExpression; import org.codehaus.groovy.ast.expr.PropertyExpression; import org.codehaus.groovy.ast.expr.StaticMethodCallExpression; import org.codehaus.groovy.ast.expr.VariableExpression; import org.codehaus.groovy.ast.stmt.CatchStatement; import org.codehaus.groovy.ast.stmt.ForStatement; import org.codehaus.groovy.eclipse.GroovyLogManager; import org.codehaus.groovy.eclipse.GroovyPlugin; import org.codehaus.groovy.eclipse.TraceCategory; import org.codehaus.groovy.eclipse.core.util.ArrayUtils; import org.codehaus.groovy.eclipse.refactoring.actions.TypeSearch.UnresolvedTypeData; import org.codehaus.jdt.groovy.model.GroovyCompilationUnit; import org.codehaus.jdt.groovy.model.ModuleNodeMapper.ModuleNodeInfo; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.SubMonitor; import org.eclipse.jdt.core.ISourceRange; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.SourceRange; import org.eclipse.jdt.core.compiler.CategorizedProblem; import org.eclipse.jdt.core.dom.rewrite.ImportRewrite; import org.eclipse.jdt.core.search.TypeNameMatch; import org.eclipse.jdt.groovy.core.util.GroovyUtils; import org.eclipse.jdt.internal.core.search.JavaSearchTypeNameMatch; import org.eclipse.jdt.internal.corext.codemanipulation.OrganizeImportsOperation.IChooseImportQuery; import org.eclipse.jdt.ui.CodeStyleConfiguration; import org.eclipse.text.edits.TextEdit; /** * Organize import operation for groovy files */ public class OrganizeGroovyImports { private static final ClassNode CLASS_NODE_FIELD = ClassHelper.make(Field.class); private class FindUnresolvedReferencesVisitor extends ClassCodeVisitorSupport { private ClassNode current; @Override protected void visitAnnotation(AnnotationNode annotation) { // skip nodes added by an annotation collector transformation if (annotation.getNodeMetaData("AnnotationCollector") == null) { handleType(annotation.getClassNode(), true); } super.visitAnnotation(annotation); } @Override public void visitArrayExpression(ArrayExpression expression) { handleType(expression.getType(), false); super.visitArrayExpression(expression); } @Override public void visitCastExpression(CastExpression expression) { handleType(expression.getType(), false); super.visitCastExpression(expression); } @Override public void visitClassExpression(ClassExpression expression) { if (expression.getEnd() > 0) { handleType(expression.getType(), false); } } @Override public void visitClosureExpression(ClosureExpression expression) { Parameter[] parameters = expression.getParameters(); if (parameters != null) { for (Parameter param : parameters) { handleType(param.getOriginType(), false); } } super.visitClosureExpression(expression); } @Override public void visitConstantExpression(ConstantExpression expression) { if (expression instanceof AnnotationConstantExpression) { handleType(expression.getType(), true); } else { // see StaticImportVisitor.transformInlineConstants(Expression) doNotRemoveImport(expression.getNodeMetaData("static.import")); } } @Override public void visitPropertyExpression(PropertyExpression expression) { if (!expression.isStatic() && !expression.isSynthetic()) { Object alias = expression.getNodeMetaData("static.import.alias"); if (alias != null) { String staticImport = expression.getText().replace('$', '.'); if (!alias.equals(expression.getPropertyAsString())) { staticImport += " as " + alias; } doNotRemoveImport(staticImport); } } super.visitPropertyExpression(expression); } @Override public void visitVariableExpression(VariableExpression expression) { if (expression.getAccessedVariable() == expression) { handleType(expression.getType(), false); } if (expression.getAccessedVariable() instanceof DynamicVariable || expression.isDynamicTyped()) { if (!checkRetainImport(expression.getName())) { // could it be static? handleVariable(expression); } } } @Override public void visitMethodCallExpression(MethodCallExpression call) { if (!call.isSynthetic() && call.getStart() > 0) { if (call.isImplicitThis()) { checkRetainImport(call.getMethodAsString()); // could it be static? } else if (call.getGenericsTypes() != null) { for (GenericsType type : call.getGenericsTypes()) { handleType(type.getType(), false); } } } super.visitMethodCallExpression(call); } @Override public void visitStaticMethodCallExpression(StaticMethodCallExpression call) { if (!call.isSynthetic() && call.getStart() > 0) { String method = call.getOwnerType().getName().replace('$', '.') + '.' + call.getMethod(); Object alias = call.getNodeMetaData("static.import.alias"); if (alias != null) { method += " as " + alias; } doNotRemoveImport(method); } super.visitStaticMethodCallExpression(call); } @Override public void visitConstructorCallExpression(ConstructorCallExpression call) { handleType(call.getType(), false); super.visitConstructorCallExpression(call); } @Override public void visitConstructor(ConstructorNode node) { if (!node.isSynthetic()) { for (Parameter param : node.getParameters()) { handleType(param.getOriginType(), false); } } super.visitConstructor(node); } @Override public void visitField(FieldNode node) { // can't check for synthetic here because it seems that non-synthetic nodes are being marked as synthetic if (node.getEnd() > 0) { handleType(node.getType(), false); // fields in a script would have Field annotation if (node.getOwner().isScript()) { handleType(CLASS_NODE_FIELD, true); } } super.visitField(node); } @Override public void visitMethod(MethodNode node) { if (!node.isSynthetic()) { handleType(node.getReturnType(), false); for (Parameter param : node.getParameters()) { handleType(param.getOriginType(), false); } ClassNode[] exceptions = node.getExceptions(); if (exceptions != null) { for (ClassNode exception : exceptions) { handleType(exception, false); } } GenericsType[] generics = node.getGenericsTypes(); if (generics != null) { for (GenericsType generic : generics) { if (!generic.isPlaceholder()) { handleType(generic.getType(), false); } else if (generic.getLowerBound() != null) { handleType(generic.getLowerBound(), false); } else if (generic.getUpperBounds() != null) { for (ClassNode upper : generic.getUpperBounds()) { handleType(upper, false); } } } } } super.visitMethod(node); } @Override public void visitClass(ClassNode node) { current = node; if (!node.isSynthetic()) { handleType(node.getSuperClass(), false); for (ClassNode impls : node.getInterfaces()) { handleType(impls, false); } // GRECLIPSE-1693 GenericsType[] generics = node.getUnresolvedSuperClass().getGenericsTypes(); if (generics != null) { for (GenericsType generic : generics) { handleType(generic.getType(), false); } } } super.visitClass(node); } @Override public void visitCatchStatement(CatchStatement node) { handleType(node.getVariable().getType(), false); super.visitCatchStatement(node); } @Override public void visitForLoop(ForStatement node) { if (!(node.getCollectionExpression() instanceof ClosureListExpression)) { // check the type node of "for (Item i in x)" but skip "for (i in x)" ClassNode type = node.getVariable().getOriginType(); if (type.getStart() > 0) { handleType(type, false); } } super.visitForLoop(node); } // /** * Assume dynamic variables are a candidate for organize imports, but * only if name begins with a capital letter and does not match the * idiomatic static constant naming. This will hopefully filter out * most false positives, but will miss types that start with lower case. */ private void handleVariable(VariableExpression expr) { String name = expr.getName(); if (!missingTypes.containsKey(name) && Character.isUpperCase(name.charAt(0)) && !STATIC_CONSTANT.matcher(name).matches()) { missingTypes.put(name, new UnresolvedTypeData(name, false, new SourceRange(expr.getStart(), expr.getEnd() - expr.getStart()))); } } /** * Adds the type name to missingTypes if it is not resolved or ensures * that the import will be retained if the type is resolved. */ private void handleType(ClassNode node, boolean isAnnotation) { if (getBaseType(node).isPrimitive()) { return; } GenericsType[] generics = node.getGenericsTypes(); int start = node.getNameStart(), until = node.getNameEnd(); if (until < 1) { start = node.getStart(); until = node.getEnd()-1; // getEnd() includes generics; try to constrain the range if (generics != null && generics.length > 0) { if (generics[0].getStart() > 0) until = generics[0].getStart() - 1; } else if (node.isArray() && getBaseType(node).getEnd() > 0) { assert start <= getBaseType(node).getStart(); assert until <= 0 || getBaseType(node).getEnd() < until; start = getBaseType(node).getStart(); until = getBaseType(node).getEnd(); } } int length = until - start; String name = getTypeName(node); // check node's generics types if (node.isUsingGenerics() && generics != null && generics.length > 0) { for (GenericsType gt : generics) { if (!gt.isPlaceholder() && !gt.isWildcard()) { handleType(gt.getType(), false); } if (gt.getLowerBound() != null) { handleType(gt.getLowerBound(), false); } else if (gt.getUpperBounds() != null) { for (ClassNode upper : gt.getUpperBounds()) { // handle enums where the upper bound is the same as the type if (!upper.getName().equals(node.getName())) { handleType(upper, false); } } } } } if (!node.isResolved() && node.redirect() != current) { // aliases come through as unresolved types if (ALIASED_IMPORT.matcher(name).find()) { doNotRemoveImport(name); return; } String[] parts = name.split("\\."); if (Character.isUpperCase(name.charAt(0))) { name = parts[0]; // Map.Entry -> Map } else if (length < name.length()) { // name range too small to include the full name doNotRemoveImport(name); // keep import name = ArrayUtils.lastElement(parts); // foo.Bar -> Bar } if (!missingTypes.containsKey(name)) { SourceRange range = new SourceRange(node.getStart(), node.getEnd() - node.getStart()); missingTypes.put(name, new UnresolvedTypeData(name, isAnnotation, range)); } } else if (length < name.length()) { // We don't know exactly what the text is. We just know how it // resolves. This can be a problem if an inner class. We don't // really know what is in the text and we don't really know what // is the import. So, just ensure that none are slated for removal. String partialName = name.replace('$', '.'); doNotRemoveImport(partialName); int innerIndex = name.length(); while ((innerIndex = name.lastIndexOf('$', innerIndex - 1)) > -1) { // 'java.util.Map.Entry' -> 'java.util.Map' as well as // 'java.util.Map.Entry as Foo.Entry' -> 'java.util.Map as Foo' partialName = name.replaceAll("\\Q" + name.substring(innerIndex) + "\\E(\\b|$)", "").replace('$', '.'); doNotRemoveImport(partialName); } } else if (length > name.length()) { GroovyPlugin.getDefault().logError(String.format( "Expected a fully-qualified name for %s at [%d..%d] line %d, but source length (%d) > name length (%d)%n", name, start, until, node.getLineNumber(), length, name.length()), new Exception()); } } private String getTypeName(ClassNode node) { ClassNode type = getBaseType(node); // unresolved name may have dots and/or dollars (e.g. 'a.b.C$D' or 'C$D' or even 'C.D') if (!type.getName().matches(".*\\b" + type.getUnresolvedName().replace('$', '.'))) { // synch up name and unresolved name (e.g. 'java.util.Map$Entry as Foo$Entry') return type.getName() + " as " + type.getUnresolvedName().replace('.', '$'); } return type.getName(); } private boolean checkRetainImport(String name) { if (!importsSlatedForRemoval.isEmpty() && !"this".equals(name) && !"super".equals(name)) { String suffix = '.' + name; for (Map.Entry<String, ImportNode> entry : importsSlatedForRemoval.entrySet()) { if (entry.getValue().isStatic() && entry.getKey().endsWith(suffix)) { doNotRemoveImport(entry.getKey()); return true; } } } return false; } private void doNotRemoveImport(Object which) { importsSlatedForRemoval.remove(which); } } private static final Pattern ALIASED_IMPORT = Pattern.compile("\\sas\\s"); private static final Pattern STATIC_CONSTANT = Pattern.compile("[A-Z][A-Z0-9_]+"); //-------------------------------------------------------------------------- private final SubMonitor monitor; private IChooseImportQuery query; private final GroovyCompilationUnit unit; private Map<String, UnresolvedTypeData> missingTypes; private Map<String, ImportNode> importsSlatedForRemoval; public OrganizeGroovyImports(GroovyCompilationUnit unit, IChooseImportQuery query) { this(unit, query, null); } public OrganizeGroovyImports(GroovyCompilationUnit unit, IChooseImportQuery query, IProgressMonitor monitor) { this.unit = unit; this.query = query; this.monitor = SubMonitor.convert(monitor, "Organize import statements", 7); } public boolean calculateAndApplyMissingImports() throws JavaModelException { TextEdit edit = calculateMissingImports(); if (edit != null) { unit.applyTextEdit(edit, monitor.newChild(0)); return true; } else { return false; } } public TextEdit calculateMissingImports() { String event = null; if (GroovyLogManager.manager.hasLoggers()) { GroovyLogManager.manager.logStart(event = unit.getElementName()); GroovyLogManager.manager.log(TraceCategory.ORGANIZE_IMPORTS, event); } try { ModuleNodeInfo info = unit.getModuleInfo(true); if (info.isEmpty() || isUnclean(info, unit)) { return null; } missingTypes = new HashMap<String, UnresolvedTypeData>(); importsSlatedForRemoval = new HashMap<String, ImportNode>(); try { // Configure the import rewriter to keep all existing imports. This is different from how // JDT does organize imports, but this prevents annotations on imports from being removed. // However, this leads to GRECLIPSE-1390 where imports are no longer reordered and sorted. Iterable<ImportNode> allImports = GroovyUtils.getAllImportNodes(info.module); ImportRewrite rewriter = CodeStyleConfiguration.createImportRewrite(unit, !isSafeToReorganize(allImports)); for (ImportNode imp : allImports) { if (imp.isStar()) { if (!imp.isStatic()) { rewriter.addImport(imp.getPackageName() + "*"); } else { rewriter.addStaticImport(imp.getClassName().replace('$', '.'), "*", true); } // GRECLIPSE-929: ensure that on-demand (i.e. star) imports are never removed } else { String className = imp.getClassName().replace('$', '.'); if (!imp.isStatic()) { if (!isAliased(imp)) { rewriter.addImport(className); importsSlatedForRemoval.put(className, imp); } else { String alias = className + " as " + imp.getAlias(); rewriter.addImport(alias); importsSlatedForRemoval.put(alias, imp); } } else { if (!isAliased(imp)) { rewriter.addStaticImport(className, imp.getFieldName(), true); importsSlatedForRemoval.put(className + '.' + imp.getFieldName(), imp); } else { rewriter.addStaticImport(className, imp.getFieldName() + " as " + imp.getAlias(), true); importsSlatedForRemoval.put(className + '.' + imp.getFieldName() + " as " + imp.getAlias(), imp); } } } } monitor.worked(1); // scan for imports that are not referenced for (ClassNode clazz : (Iterable<ClassNode>) info.module.getClasses()) { GroovyClassVisitor visitor = new FindUnresolvedReferencesVisitor(); visitor.visitClass(clazz); // modifies missingTypes and importsSlatedForRemoval } monitor.worked(4); // remove all default imports for (ImportNode imp : allImports) { if (isDefaultImport(imp)) { // remove default imports String key; // not removing static imports if (imp.getClassName() != null) { key = imp.getClassName(); } else { // an on-demand/star import key = imp.getPackageName(); if (key.endsWith(".")) { key += "*"; } } importsSlatedForRemoval.put(key, imp); } } // remove imports that were not matched to a source element for (Map.Entry<String, ImportNode> entry : importsSlatedForRemoval.entrySet()) { trace("Remove import '%s'", entry.getKey()); if (!entry.getValue().isStatic()) { rewriter.removeImport(entry.getKey()); } else { rewriter.removeStaticImport(entry.getKey()); } } monitor.worked(1); // deal with the missing types if (!missingTypes.isEmpty()) { pruneMissingTypes(allImports); if (!missingTypes.isEmpty()) { monitor.subTask("Resolve missing types"); monitor.setWorkRemaining(missingTypes.size() + 1); for (IType type : resolveMissingTypes(monitor.newChild(1))) { trace("Missing type '%s'", type); rewriter.addImport(type.getFullyQualifiedName('.')); } } } TextEdit rewrite = rewriter.rewriteImports(monitor.newChild(1)); trace("%s", rewrite); return rewrite; } catch (Exception e) { GroovyPlugin.getDefault().logError("Exception thrown when organizing imports for " + unit.getElementName(), e); } finally { importsSlatedForRemoval = null; missingTypes = null; monitor.done(); } return null; } finally { if (event != null) { GroovyLogManager.manager.logEnd(event, TraceCategory.ORGANIZE_IMPORTS); } } } /** * There are cases where a type is seen as unresolved but can be found * amongst the imports of the module or within the default imports. * <p> * One such case is the use of a parameterized type, but not all type * params have been satisfied correctly. Another involves annotation * types that have not been identified correctly as annotations. */ private void pruneMissingTypes(Iterable<ImportNode> imports) throws JavaModelException { Set<String> starImports = new LinkedHashSet<String>(); Set<String> typeImports = new LinkedHashSet<String>(); if (unit.getModuleNode().getPackageName() != null) { starImports.add(unit.getModuleNode().getPackageName()); } else { starImports.add(""); } for (ImportNode in : imports) { if (!in.isStatic()) { if (in.isStar()) { starImports.add(in.getPackageName()); } else { typeImports.add(in.getText()); } } } for (String di : DEFAULT_IMPORTS) { if (di.endsWith(".")) { starImports.add(di); } else { typeImports.add(di + " as " + di.substring(di.lastIndexOf('.') + 1)); } } // check each missing type against the module's single-type and on-demand imports on: for (Iterator<String> it = missingTypes.keySet().iterator(); it.hasNext();) { String typeName = it.next(); for (String ti : typeImports) { if (ti.endsWith(' ' + typeName)) { it.remove(); continue on; } } for (String si : starImports) { IType type = unit.getJavaProject().findType(si + typeName, (IProgressMonitor) null); if (type != null) { it.remove(); continue on; } } } } private IType[] resolveMissingTypes(IProgressMonitor monitor) throws JavaModelException { // fill in all the potential matches new TypeSearch().searchForTypes(unit, missingTypes, monitor); List<TypeNameMatch> missingTypesNoChoiceRequired = new ArrayList<TypeNameMatch>(); List<TypeNameMatch[]> missingTypesChoiceRequired = new ArrayList<TypeNameMatch[]>(); List<ISourceRange> ranges = new ArrayList<ISourceRange>(); // go through all the resovled matches and look for ambiguous matches for (UnresolvedTypeData data : missingTypes.values()) { int foundInfosSize = data.foundInfos.size(); if (foundInfosSize == 1) { missingTypesNoChoiceRequired.add(data.foundInfos.get(0)); } else if (foundInfosSize > 1) { missingTypesChoiceRequired.add(data.foundInfos.toArray(new TypeNameMatch[foundInfosSize])); ranges.add(data.range); } } TypeNameMatch[][] missingTypesArr = missingTypesChoiceRequired.toArray(new TypeNameMatch[0][]); TypeNameMatch[] chosen; if (missingTypesArr.length > 0) { chosen = query.chooseImports(missingTypesArr, ranges.toArray(new ISourceRange[0])); } else { chosen = new TypeNameMatch[0]; } if (chosen != null) { IType[] typeMatches = new IType[missingTypesNoChoiceRequired.size() + chosen.length]; int index = 0; for (TypeNameMatch typeNameMatch : missingTypesNoChoiceRequired) { typeMatches[index++] = typeNameMatch.getType(); } for (int i = 0, n = chosen.length; i < n; i += 1) { typeMatches[index++] = ((JavaSearchTypeNameMatch) chosen[i]).getType(); } return typeMatches; } else { // dialog was canceled; do nothing return new IType[0]; } } /** * GRECLIPSE-1390 * Reorganizing imports (ie- sorting and grouping them) will remove annotations on import statements * In general, we want to reorganize, but it is not safe to do so if the are any annotations on imports * @param allImports all the imports in the compilation unit * @return true iff it is safe to reorganize imports */ private static boolean isSafeToReorganize(Iterable<ImportNode> allImports) { for (ImportNode imp : allImports) { if (imp.getAnnotations() != null && !imp.getAnnotations().isEmpty()) { return false; } } return true; } private static final Set<String> DEFAULT_IMPORTS = new LinkedHashSet<String>(); static { DEFAULT_IMPORTS.add("java.lang."); DEFAULT_IMPORTS.add("java.util."); DEFAULT_IMPORTS.add("java.io."); DEFAULT_IMPORTS.add("java.net."); DEFAULT_IMPORTS.add("groovy.lang."); DEFAULT_IMPORTS.add("groovy.util."); DEFAULT_IMPORTS.add("java.math.BigDecimal"); DEFAULT_IMPORTS.add("java.math.BigInteger"); } /** * Checks to see if this import statment is a default import. */ private static boolean isDefaultImport(ImportNode imp) { // not really correct since I think Math.* is a default import. but OK for now if (imp.isStatic()) { return false; } // aliased imports are not considered default if (imp.getType() != null && !imp.getType().getNameWithoutPackage().equals(imp.getAlias())) { return false; } // now get the package name String pkg; if (imp.getType() != null) { pkg = imp.getType().getPackageName(); if (pkg == null) { pkg = "."; } else { pkg = pkg + "."; } if (pkg.equals("java.math.")) { pkg = imp.getType().getName(); } } else { pkg = imp.getPackageName(); if (pkg == null) { pkg = "."; } } return DEFAULT_IMPORTS.contains(pkg); } private static boolean isAliased(ImportNode imp) { String alias = imp.getAlias(); if (alias == null) { return false; } String fieldName = imp.getFieldName(); if (fieldName != null) { return !fieldName.equals(alias); } String className = imp.getClassName(); if (className != null) { // it is possible to import from the default package boolean aliasIsSameAsClassName = className.endsWith(alias) && (className.length() == alias.length() || className.endsWith("." + alias) || className.endsWith("$" + alias)); return !aliasIsSameAsClassName; } return false; } /** Determines if organize imports is unsafe due to syntax errors or other conditions. */ private static boolean isUnclean(ModuleNodeInfo info, GroovyCompilationUnit unit) { try { if (info.module.encounteredUnrecoverableError() || !unit.isConsistent()) { return true; } CategorizedProblem[] problems = info.result.getProblems(); if (problems != null && problems.length > 0) { for (CategorizedProblem problem : problems) { if (problem.isError() && problem.getCategoryID() == CategorizedProblem.CAT_INTERNAL) { String message = problem.getMessage(); if (message.contains("unexpected token")) { trace("Stopping due to error in compilation unit: %s", message); return true; } } } } } catch (Exception e) { return true; } return false; } private static void trace(String message, Object... arguments) { if (GroovyLogManager.manager.hasLoggers()) { GroovyLogManager.manager.log(TraceCategory.ORGANIZE_IMPORTS, String.format(message, arguments)); } } }