1. Wyszukiwanie plików

Należało zastosować rekurencję. Wygodniejsze jest przy tym listFiles(...) niż list(..). Do wyboru plików - FileFilter. Co to są pliki html - tak naprawdę nie jest to jednoznaczne. Wydaje się wystarczające zastosowanie zestawu rozszerzeń, zadawanego przez użytkownika. Przy czym należy uwzględnić, że wielkość liter nie ma znaczenia.


2. Komunikacja między wątkami

Najprostsza wydaje się komunikacja przez potok. Przy buforowaniu trzeba dobrać rozmiar bufora tak, by wątki działały "wizualnie" równolegle (przy dużym, niewymiatanym buforze, wątki mogą działać "napzremiennie" - np. wyszukiwacz znajduje najpierw 200 plików, po czym konwerter je przetwarza, po czym wyszukiwacz znowu szuka itd.)

3. Test

Dostarczyłem klasy testowej, która w GUI opartym na AWT pokazuje postępy szukania i przetwarzania. To nie jest oczywiście prawdziwe GUI dla takiego zadania, ale przydaje się przy testowaniiu.

Poniżej fragmenty kodu.


// Klasa dziedziczona przez inne klasy wątków
// Daje jednolity sposób kończenia pracy wątku.
// Proszę zwrócić uwagę na synchronizowanie metod

class StopableThread extends Thread {
  
  private boolean stopped = false;
  
    
  public synchronized boolean isStopped() {
    return stopped;
  }
  
  public synchronized void finish() {
    stopped = true;
  }
  
}

Klasa watku wyszukiwania plików.

import java.util.*;
import java.io.*;

class FileSearcher extends StopableThread {
  
  private String[] ext;       // tablica rozszerzeń plików
  private File initDir;       // katalog początkowy
  private BufferedWriter out; // buforowane wyjście
  private int count;          // liczba odnalezionych plików
  Test gui;                   // Gui do pokazywania postępów
    
  public FileSearcher(String beg, String[] exts, Writer outPipe, Test gui) {
    ext = new String[exts.length];
    for (int i=0; i< exts.length; i++) 
      ext[i] = exts[i];
    initDir = new File(beg);
    out = new BufferedWriter(outPipe, 256);
    this.gui = gui;
  }
  
  public void run() {
    try {
      srchFiles(initDir);      // szukamy plików 
      out.write("***FINISH");  // gdy szukanie skończone - zapis FINISH do potoku
      out.newLine();            
      out.close();
    } catch(IOException exc) {
        String msg = exc.getMessage();
        
        // Wymienione wyjątki oznaczają wcześniejsze zakończenie
        // konwertera i powinny spowodowac łagodne zkończenie wyszukiwania
        if (!msg.equals("Read end dead") && !msg.equals("Pipe broken")) {
          exc.printStackTrace();
          System.exit(1);
        }
      }
    System.out.println("Koniec FileSrch");  
  }
  
  // Metoda szukania plików - rekurencja!
  
  private void srchFiles(File srchDir) throws IOException {
    if (isStopped()) return;
    File[] files = srchDir.listFiles(new FileFilter() {
        // akceptujemy katalogi i pliki o rozszerzeniach z tablicy
        public boolean accept(File file) {
          if (file.isDirectory()) return true;
          String fname = file.toString();
          int dot = fname.lastIndexOf(".");
          String extension = fname.substring(dot+1).toLowerCase();
          for (int i=0; i<ext.length; i++) {
            if (extension.equals(ext[i])) return true;
            }
          return false;
       }
    });
    
    if (files != null) { // null może być wtedy, gdy mamy wadliwy katalog inicj.
                         // lub gdy nastąpił błąd I/) 
                         // np. sięgamy do ukrytego katalogu
                         
      for (int i=0; i < files.length && !isStopped(); i++) {
          
          // Jeśli katalog - przeszukujemy go
          if (files[i].isDirectory()) srchFiles(files[i]);  
          
          else { // plik HTML
            gui.showFound("Found: " + ++count); // info do GUI
            out.write(files[i].toString());     // zapis nazwy do potoku 
            out.newLine();
            yield();
          }
      }
    }
   }   
}

Klasa wątku konwersji plików
import java.util.regex.*;
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;

class HtmlConverter extends StopableThread {
	
	private Test gui;               // gui pokazujące postępy
  private BufferedReader inpipe;  // potok wejściowy (buforowany)
  private int count = 0;          // liczba przetworzonych plików
  private String destDir;         // katalog docelowy do zapisu skonwertowanych

  // Konstruktor		
  public HtmlConverter(Test t, Reader pipeIn, String dir) {
    gui = t;
    inpipe = new BufferedReader(pipeIn,256);
    destDir = dir;
  }
  
  // Kod wątku
  public void run() {
    String line = "";
    while (!isStopped()) {
      try {
         line = inpipe.readLine();
         if (line == null) break;
         if (line.equals("***FINISH")) break;
         if (convert(line)) gui.show("Converted: " + line);
         gui.showProcessed("Processed: " + ++count);  
         
      } catch(IOException exc) { 
          exc.printStackTrace();
          break;
      }
    }
    System.out.println("Koniec convert");
  }    

  // ---- Zmienne ważne dla metody convert (podanej dalej)

  // Separator plików (/ lub \) na danej platformie
  private String fileSep = System.getProperty("file.separator");

  // Wzorce będą kompilowane raz przy tworzeniu obiektu 
  // wzorzec do podziału pliku na nagłówek i resztę
  private Pattern 
     splitPatt = Pattern.compile("</head>", Pattern.CASE_INSENSITIVE);
  
  // wzorzec do odnalezienia charset win 1250 w nagłówku
  // i podziału nagłowka na trzy częsci, by podmienić "charset=..."   
  private Pattern    
     winPatt = Pattern.compile(
       "(.+?)(charset[ ]*?=[ ]*?windows[ ]*?-[ ]*?1250)(.*?)",
                       Pattern.DOTALL | Pattern.CASE_INSENSITIVE); 
  
  // Strony kodowe
  private Charset inCharset  = Charset.forName("Cp1250");
  private Charset outCh = Charset.forName("ISO-8859-2");
  
  // Metoda konwertująca plik o podanej nazwie
  
  private boolean convert(String fname) throws IOException {

    // Wczytanie pliku
    FileInputStream instream = new FileInputStream(fname);
    FileChannel in = instream.getChannel();
    ByteBuffer buf = ByteBuffer.allocate((int) in.size());
    in.read(buf); in.close(); instream.close();

    // Dekodowanie
    buf.flip();
    CharBuffer cb = inCharset.decode(buf);

    // Wzorzec </head> będzie zastosowany raz do wyodrębnienia
    // nagłówka
    String[] part = splitPatt.split(cb,2); // part[0] - nagłówek
    if (part.length != 2) return false;    // brak tagu </head>
    String body = part[1];                 // reszta pliku HTML 
    
    // czy w nagłówku znajdziemy określenie strony kodowej win-1250

    Matcher m = winPatt.matcher(part[0]);
    boolean result = m.matches();
    
    if (result) { // jeżeli znaleziony
      
      // Odtworzenie nagłówka
      // z zamianą "charset ="
      // korzystamy z grup zapamiętanych przy "pasowaniu" wyrażenia regularnego
      StringBuffer header = new StringBuffer(m.group(1));
      header.append("charset=ISO-8859-2");
      header.append(m.group(3));
      
      // Utworzenie nowego obiektu plikowego (będzie w katalogu destDir)
      // z nazwą, która ew. będzie zawierać numerację dla odróżnienia
      // istniejących plików
      int p = fname.lastIndexOf(fileSep);
      int dot = fname.lastIndexOf(".");
      String ext = fname.substring(dot);
      StringBuffer outFn = new StringBuffer(destDir);
      outFn.append("/");
      outFn.append(fname.substring(p+1, dot));
      File outf = new File(outFn+ext);
      int nr = 1;
      while (outf.exists()) {
        outf = new File(outFn + "_" + (nr++) + ext);
      }
      
      // Enkodowanie 
      ByteBuffer[] outBuff = {
             outCh.encode(CharBuffer.wrap(header)),
             outCh.encode(CharBuffer.wrap(body))
             };
      // Zapis - gathering write! 
      FileChannel out = new FileOutputStream(outf).getChannel();
      out.write(outBuff);
      out.force(false);
      out.close();
    }
    return result;
  }
  

}

Klasa testująca

import java.awt.*;
import java.awt.event.*;
import java.io.*;

class Test extends Frame implements ActionListener {
  
  // Parametry do zmiany
  // --- rozszerzenia plików branych pod uwagę
  private static final String[] EXTS  = { "htm", "html", "shtml" };
  // --- katalog w którym zaczyna się rekursywne wyszukiwanie
  private static final String INIT_DIR = "G:/";
  // --- katalog docelowy (do zapisu skonwertowanych plików)
  private static final String DEST_DIR = "G:/temp";
  
  private TextArea ta = new TextArea(25, 30);  // obszar komunikatów
  private FileSearcher fs;                     // wyszukiwacz
  private HtmlConverter hc;                    // konwerter 
  private TextField found = new TextField(12); // pola informacyjne
  private TextField proc = new TextField(12);
  
  public Test() {
    PipedWriter pipeOut = new PipedWriter();
    PipedReader pipeIn = null;
    try {
      pipeIn = new PipedReader(pipeOut);
    } catch(IOException exc) { exc.printStackTrace(); System.exit(1); }

    // Wyszukiwacz:
    // - startuje od ktalogu INIT_DIR
    // - podaje pliki o rozszerzeniach z tablicy EXTS
    // - do potoku pipeOut
    // - pokazując postępy w tym GUI (this) 
    fs = new FileSearcher(INIT_DIR, EXTS, pipeOut, this);
    
    // Konwerter 
    // - pokaże postępy tu (this)
    // - odczytuje nazwy plików do konwersji z potoku pipeIn
    // zapisze nowe wersje plikow w katalogu DEST_DIR
    hc = new HtmlConverter(this, pipeIn, DEST_DIR);
    
    Button b = new Button("Stop");  // tym przyciskiem można zakończyć wątki
    b.addActionListener(this);      
    add(ta, "Center");
    Panel p = new Panel(new FlowLayout());
    p.add(b);
    p.add(found);
    p.add(proc);
    add(p, "South");
    // Zamknięcie okna - zakończenie aplikacji
    addWindowListener( new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        dispose();
        System.exit(0);
      }
    });
    
    // Start wątków
    fs.start();
    hc.start();
    
    // Okno
    pack();
    show();
  }
  
  // Metoda, która może być używana w ątkach
  // wypisuje info w obsarze komunikatów
  public synchronized void show(String s) {
    ta.append(s+ '\n');
  }
  
  // Metodw wołana z wątku wyszukiwania
  // pokazuje postępy w polu informacyjnym 
  public synchronized void showFound(String s) {
    found.setText(s);
  }

  // Metodw wołana z wątku konwersji
  // pokazuje postępy w polu informacyjnym 
  public synchronized void showProcessed(String s) {
    proc.setText(s);
  }

  // Obsługa wyboru przycisku STOP
  // kończy oba wątki  
  public void actionPerformed(ActionEvent e) {
    fs.finish();
    hc.finish();
  }
    

  public static void main(String args[]) {
    new Test();   
  }
}