В тази секция ще погледнем по-завършен пример за използване на Java IO. Този проект е пряко полезен понеже изпълнява проверка дали вашите файлове отговарят на Java стила на писане както може да се намери на www.JavaSoft.com. Отваря всеки .java файл в текущата директория и извлича ивената на класове и идентификаторите, после показва ако нещо не отговаря на Java стила.
За да работи програмата коректно, трябва първо да построите хранилище за имена на класове, което да съдържа всички имена от Java библиотеката. Това се прави чрез преминаване на всичкия сорс във всичките поддиректории на Java библиотеката и пускане на ClassScanner за всяка поддиректория. Давайки като аргументи имената на файловете-хранилища (с един и същ път и име всеки път) и -a опция на командния ред за отбелязване че имената на класове ще се добавят в хранилището.
За да се използва програмата за проверка на код, дайте име на файл и хранилище за използване и я пуснете. Тя ще провери всички имена на файлове и идентификатори в текущата директория и ще ви каже кои не следват типичния за Java стил на капитализация.
Трябва да знаете, че програмата не е перфектна; понякога ще показва че има проблеми, но при преглед ще установите, че няма такива. Това е малко ядосващо, но е много по-добре като цяло, отколкото да се намерят всички тези имена непосредствено от кода.
Обяснението непосредствено следва листинга:
//: c10:ClassScanner.java
// Scans all files in directory for classes
// and identifiers, to check capitalization.
// Assumes properly compiling code listings.
// Doesn't do everything right, but is a very
// useful aid.
import java.io.*;
import java.util.*;
class MultiStringMap extends HashMap {
public void add(String key, String value) {
if(!containsKey(key))
put(key, new ArrayList());
((ArrayList)get(key)).add(value);
}
public ArrayList getArrayList(String key) {
if(!containsKey(key)) {
System.err.println(
"ERROR: can't find key: " + key);
System.exit(1);
}
return (ArrayList)get(key);
}
public void printValues(PrintStream p) {
Iterator k = keySet().iterator();
while(k.hasNext()) {
String oneKey = (String)k.next();
ArrayList val = getArrayList(oneKey);
for(int i = 0; i < val.size(); i++)
p.println((String)val.get(i));
}
}
}
public class ClassScanner {
private File path;
private String[] fileList;
private Properties classes = new Properties();
private MultiStringMap
classMap = new MultiStringMap(),
identMap = new MultiStringMap();
private StreamTokenizer in;
public ClassScanner() {
path = new File(".");
fileList = path.list(new JavaFilter());
for(int i = 0; i < fileList.length; i++) {
System.out.println(fileList[i]);
scanListing(fileList[i]);
}
}
void scanListing(String fname) {
try {
in = new StreamTokenizer(
new BufferedReader(
new FileReader(fname)));
// Doesn't seem to work:
// in.slashStarComments(true);
// in.slashSlashComments(true);
in.ordinaryChar('/');
in.ordinaryChar('.');
in.wordChars('_', '_');
in.eolIsSignificant(true);
while(in.nextToken() !=
StreamTokenizer.TT_EOF) {
if(in.ttype == '/')
eatComments();
else if(in.ttype ==
StreamTokenizer.TT_WORD) {
if(in.sval.equals("class") ||
in.sval.equals("interface")) {
// Get class name:
while(in.nextToken() !=
StreamTokenizer.TT_EOF
&& in.ttype !=
StreamTokenizer.TT_WORD)
;
classes.put(in.sval, in.sval);
classMap.add(fname, in.sval);
}
if(in.sval.equals("import") ||
in.sval.equals("package"))
discardLine();
else // It's an identifier or keyword
identMap.add(fname, in.sval);
}
}
} catch(IOException e) {
e.printStackTrace();
}
}
void discardLine() {
try {
while(in.nextToken() !=
StreamTokenizer.TT_EOF
&& in.ttype !=
StreamTokenizer.TT_EOL)
; // Throw away tokens to end of line
} catch(IOException e) {
e.printStackTrace();
}
}
// StreamTokenizer's comment removal seemed
// to be broken. This extracts them:
void eatComments() {
try {
if(in.nextToken() !=
StreamTokenizer.TT_EOF) {
if(in.ttype == '/')
discardLine();
else if(in.ttype != '*')
in.pushBack();
else
while(true) {
if(in.nextToken() ==
StreamTokenizer.TT_EOF)
break;
if(in.ttype == '*')
if(in.nextToken() !=
StreamTokenizer.TT_EOF
&& in.ttype == '/')
break;
}
}
} catch(IOException e) {
e.printStackTrace();
}
}
public String[] classNames() {
String[] result = new String[classes.size()];
Iterator e = classes.keySet().iterator();
int i = 0;
while(e.hasNext())
result[i++] = (String)e.next();
return result;
}
public void checkClassNames() {
Iterator files = classMap.keySet().iterator();
while(files.hasNext()) {
String file = (String)files.next();
ArrayList cls = classMap.getArrayList(file);
for(int i = 0; i < cls.size(); i++) {
String className =
(String)cls.get(i);
if(Character.isLowerCase(
className.charAt(0)))
System.out.println(
"class capitalization error, file: "
+ file + ", class: "
+ className);
}
}
}
public void checkIdentNames() {
Iterator files = identMap.keySet().iterator();
ArrayList reportSet = new ArrayList();
while(files.hasNext()) {
String file = (String)files.next();
ArrayList ids = identMap.getArrayList(file);
for(int i = 0; i < ids.size(); i++) {
String id =
(String)ids.get(i);
if(!classes.contains(id)) {
// Ignore identifiers of length 3 or
// longer that are all uppercase
// (probably static final values):
if(id.length() >= 3 &&
id.equals(
id.toUpperCase()))
continue;
// Check to see if first char is upper:
if(Character.isUpperCase(id.charAt(0))){
if(reportSet.indexOf(file + id)
== -1){ // Not reported yet
reportSet.add(file + id);
System.out.println(
"Ident capitalization error in:"
+ file + ", ident: " + id);
}
}
}
}
}
}
static final String usage =
"Usage: \n" +
"ClassScanner classnames -a\n" +
"\tAdds all the class names in this \n" +
"\tdirectory to the repository file \n" +
"\tcalled 'classnames'\n" +
"ClassScanner classnames\n" +
"\tChecks all the java files in this \n" +
"\tdirectory for capitalization errors, \n" +
"\tusing the repository file 'classnames'";
private static void usage() {
System.err.println(usage);
System.exit(1);
}
public static void main(String[] args) {
if(args.length < 1 || args.length > 2)
usage();
ClassScanner c = new ClassScanner();
File old = new File(args[0]);
if(old.exists()) {
try {
// Try to open an existing
// properties file:
InputStream oldlist =
new BufferedInputStream(
new FileInputStream(old));
c.classes.load(oldlist);
oldlist.close();
} catch(IOException e) {
System.err.println("Could not open "
+ old + " for reading");
System.exit(1);
}
}
if(args.length == 1) {
c.checkClassNames();
c.checkIdentNames();
}
// Write the class names to a repository:
if(args.length == 2) {
if(!args[1].equals("-a"))
usage();
try {
BufferedOutputStream out =
new BufferedOutputStream(
new FileOutputStream(args[0]));
c.classes.save(out,
"Classes found by ClassScanner.java");
out.close();
} catch(IOException e) {
System.err.println(
"Could not write " + args[0]);
System.exit(1);
}
}
}
}
class JavaFilter implements FilenameFilter {
public boolean accept(File dir, String name) {
// Strip path information:
String f = new File(name).getName();
return f.trim().endsWith(".java");
}
} ///:~
Класът MultiStringMap'>MultiStringMap е инструмент който позволява да се проектира група стрингове върху отделен ключ. Както в предишния пример, използва се HashMap (този път с наследяване) с ключа като единствен стринг който се проектира в ArrayList стойност. Методът add( ) просто проверява дали вече има такъв ключ в HashMap, ако няма го слага там. Методът getArrayList( ) дава ArrayList за конкретен ключ, а printValues( ), който основно е полезен за тестване, извежда всички стойности ArrayList по ArrayList.
За да се опрости живота, имената от стандартните Java библиотеки са пъхнати в Properties обект (от стандартната Java библиотека). Помнете че Properties обектът е HashMap който съдържа само String обекти и за ключа, и за стойността. Обаче той може да бъде запазван на диск и възстановяван от там с едно извикване на метод, така че е идеален за склад на имена. Фактически се нуждаем само от списък с имена и HashMap не може да приеме null за ключ или стойност. Така че един и същ обек ще се използва и за стойностите, и за ключовете.
За класовете във файловете от конкретна директория се използват два, two MultiStringMapа: classMap и identMap. Също когато програмата тръгва тя товари склада за имена в Properties обект наречен classes, а когато се намери ново име на клас, то се добавя също към classes както и към classMap. По този начин classMap може да бъде използван за преминаване по всички класове в локалната директория, а classes може да бъде използван за проверка дали текущият токен е име на клас (което показва че започва дефиниция на обект или метод, така че се грабват следващите токени – до точка и запетая – и се слагат в identMap).
Конструкторът по подразбиране на ClassScanner създава списък от файлови имена (чрез JavaFilter реализацията на FilenameFilter, както е описано в глава 10). После вика scanListing( ) за всяко класово име.
Вътре в scanListing( ) сорсовия файл е отворен и се превръща в StreamTokenizer. По документация чрез даване на true на slashStarComments( ) и slashSlashComments( ) се очаква да се махнат тези коментари, но това май не е точно така (не работи в Java 1.0). Вместо това тези редове се изкоментират и се извличат от друг метод. За да стане това ‘/’ трябва да бъде хванато като обикновен знак вместо да се остави StreamTokenizer да го погълне като част от коментар, а ordinaryChar( ) методът казва на StreamTokenizer да направи това. Това също е вярно за точки (‘.’), понеже искаме извикванията на методи да са разпаднати на отделни идентификатори. Обаче подчертаващото тире, което обикновено се третира от StreamTokenizer като отделен знак, ще се остави като част от идентификатора, понеже се появява в такива static final стойности като TT_EOF и т.н., използвани в същата тази програма. Методът wordChars( ) взема количество знаци които искате да се добавят към онези които се оставят вътре в обработвания токен като една дума. Накрая, когато обработваме едноредов коментар или пренебрегваме ред трябва да знаем къде е знакът за край на ред, та чрез викане на eolIsSignificant(true) eol ще се покаже наместо да бъде погълнат от StreamTokenizer.
Останалото от scanListing( ) чете и реагира на токените до края на файла, означаванс връщане от nextToken( ) на final static стойност StreamTokenizer.TT_EOF.
Ако токенът е ‘/’ това потенциално е коментар, така че eatComments( ) се вика да се разправя с него. Единствената друга ситуация от която сме заинтересовани тук е когато е в дума, като има няколко специални случая.
Ако думата е class или interface тогава следващият токен представя име на клас или интерфейс и се слага в classes и classMap. Ако думата е import или package, не искаме останалата част от реда. Всичко друго трябва да е идентификатор (от който се интересуваме) или ключова дума (от които не се интересуваме, но в редки случаи са изцяло с малки букви, така че не е лошо да се вкарат, за да не развалят работата). Всички се добавят в identMap.
Методът discardLine( ) е прост инструмент който следи за край на ред. Забележете че всеки път когато имате нов токен трябва да проверите за край на файла.
Методът eatComments( ) се вика винаги когато в основния цикъл на преглеждане се срещне наклонена черта. Това обаче не значи непременно че е намерен коментар, така че трябва да се види следващия токен за да се види дали е наклонена черта (в който случай редът се пренебрегва) или звезда. Но ако не е от тези, това значи че чертата трябва да се вкара обратно в главния цикъл! За щастие методът pushBack( ) позволява да “се вкара обратно” текущия токен на входния поток така че когато главният цикъл вика nextToken( ) той ще го намери бутнат обратно.
За удобство методът classNames( ) дава масив от всичките имена в колекцията classes. Този метод не се използва в програмата но е полезен при тестване.
Следващите два метода са където фактически се прави проверката. В checkClassNames( ) имената на класове се вземат от classMap (който, помнете, съдържа имената само от тази директория, организирани по файловото име така, че то може да бъде изведено заедно с грешното име на клас). Това се прави чрез слагане на всеки асоцииран ArrayList и преглеждането му, за да се види дали първият знак е малка буква. Ако да, извежда се съответното съобщение за грешка.
В checkIdentNames( ) има подобен подход: всяко име на идентификатор се взема от identMap. Ако името не е в списъка на classes, счита се че е идентификатор или ключова дума. Проверява се един специален случай: ако дължината на идентификатора е 3 или повече и всичките знаци са главни букви, този идентификатор се игнорира понеже вероятно е static final стойност като например TT_EOF. Разбира се, това не е перфектен алгоритъм, но той предполага че ще забележите в края на краищата всички думи само от главни букви.
Вместо да докладва всеки идентификатор който започва с главна буква, този метод следи кои вече са били докладвани в ArrayList наречен reportSet( ). Това третира ArrayList като “множество” което ви казва дали елементът вече е в множеството. Елементът се получава чрез конкатенация на файловото име и идентификатора. Ако елементът не е в множеството, добавя се и после се докладва.
Останалата част от листинга включва main( ), който е зает с обработката на аргументите на командния ред и установяване дали искате да правите склад за имена на класове от стандартна Java библиотека или да проверявате валидността на написан от вас код. В двата случая се прави ClassScanner обект.
Дали правите склад или използвате такъв, трябва да отворите съществуващия склад. Чрез провене на File обект и проверка за съществуване може да решите дали да отворите файла и load( ) списъка Properties classes вътре в ClassScanner. (Класовете от склада се добавят към, а не подтискат класовете от конструктора на ClassScanner.) Ако дадете един аргумент на командния ред това значи че искате да направите проверка на имената на класове и идентификатори, но ако дадете два аргумента (вторият “-a”) правите склад за имена на класове. В този случай се отваря файл за изход и Properties.save( ) се използва за писане на списъка във файл, заедно със стринг, който съдържа заглавна информация за файла.
Сподели с приятели: |