вторник, 11 октября 2011 г.

Как получить mime-type файла в Java

Теоретически есть три способа получить информацию о типе файла. Все эти способы имеют свои преимущества и, конечно же, недостатки.

Способ первый: не заглядываем внутрь
Проще всего определить тип файла по его расширению. Знакомая с детства, понятная, быстрая и весьма неточная схема. Собственно, точность зависит от того, насколько полным и актуальным является используемый нами справочник расширений. Чтобы не хардкодить свой справочник используем, например javax.activation.MimetypesFileTypeMap:
import javax.activation.MimetypesFileTypeMap;
import java.io.File;

class GetMimeType {
 public static void main(String args[]) {
  File f = new File("gumby.gif");
  System.out.println("Mime Type of " + f.getName() + " is " +
             new MimetypesFileTypeMap().getContentType(f));
  // expected output :
  // "Mime Type of gumby.gif is image/gif"
 }
}

Для работы этого кода потребуется подключить библиотеку activation.jar, которую можно взять отсюда.
Библиотека ищет данные о типе файла в нескольких местах системы в следующем порядке:
  1. Programmatically added entries to the MimetypesFileTypeMap instance.
  2. The file .mime.types in the user's home directory.
  3. The file /lib/mime.types.
  4. The file or resources named META-INF/mime.types.
  5. The file or resource named META-INF/mimetypes.default (usually found only in the activation.jar file). 
Способ второй: Чтобы понять что это, соединимся с этим
Способ более надёжный, не требует дополнительных библиотек, но не рекомендуется к использованию в "промышленных масштабах" ввиду крайней медлительности.
import java.net.*;

public class FileUtils{
 public static String getMimeType(String fileUrl)
  throws java.io.IOException, MalformedURLException
 {
  String type = null;
  URL u = new URL(fileUrl);
  URLConnection uc = null;
  uc = u.openConnection();
  type = uc.getContentType();
  return type;
 }

 public static void main(String args[]) throws Exception {
  System.out.println(FileUtils.getMimeType("file://c:/temp/test.TXT"));
  // output : text/plain
 }
}

Тут мы практически открываем соединение с файлом по протоколу file:// и читаем заголовок content-type из полученного соединения.
Чуть более быстрый способ:

import java.net.FileNameMap;
import java.net.URLConnection;

public class FileUtils {

 public static String getMimeType(String fileUrl)
   throws java.io.IOException
  {
   FileNameMap fileNameMap = URLConnection.getFileNameMap();
   String type = fileNameMap.getContentTypeFor(fileUrl);

   return type;
  }

  public static void main(String args[]) throws Exception {
   System.out.println(FileUtils.getMimeType("file://c:/temp/test.TXT"));
   // output : text/plain
  }
 }
Принцип тут тот же.

Способ третий: Посмотрим-таки внутрь!
Самый надёжный способ, определяющий тип файла по его содержимому. Точен настолько, насколько это возможно, достаточно быстр. Но требует использования большого числа  сторонних библиотек. Например, используя Apache Tika:

import java.io.File;
import java.io.FileInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.Parser;
import org.apache.tika.sax.BodyContentHandler;
import org.xml.sax.ContentHandler;

public class Main {

  public static void main(String args[]) throws Exception {

  FileInputStream is = null;
  try {
   File f = new File("C:/Temp/mime/test.docx");
   is = new FileInputStream(f);

   ContentHandler contenthandler = new BodyContentHandler();
   Metadata metadata = new Metadata();
   metadata.set(Metadata.RESOURCE_NAME_KEY, f.getName());
   Parser parser = new AutoDetectParser();
   // OOXMLParser parser = new OOXMLParser();
   parser.parse(is, contenthandler, metadata);
   System.out.println("Mime: " + metadata.get(Metadata.CONTENT_TYPE));
   System.out.println("Title: " + metadata.get(Metadata.TITLE));
   System.out.println("Author: " + metadata.get(Metadata.AUTHOR));
   System.out.println("content: " + contenthandler.toString());
  }
  catch (Exception e) {
   e.printStackTrace();
  }
  finally {
    if (is != null) is.close();
  }
 }
}
Библиотека - часть поискового движка Lucene - подключает в рантайме около 20 (!) зависимостей (в общей сложности мегабайт на 18). Смотрите сами, нужно ли оно вам в проекте.

Более компактный вариант - использовать библиотеку JMimeMagic:

  public static String getMime(String path) {
    MagicMatch match = null;
    try {
      match = Magic.getMagicMatch(new File(path), true);
    } catch (Exception e){
      e.printStackTrace(System.err);
    }
    return match.getMimeType();
  }
Плюсы: всего две зависимости: Apache Common logging и Jakarta Oro (уже не разрабатывается). Вместе с зависимостями размер библиотеки едва превышает 150 Kb.
Минусы: сравнительно невысокая точность - "не узнаёт" несколько популярных форматов файлов, иногда "убивает" наше приложение с ошибкой OutOfMemoryError. На исправление ошибок вряд ли стоит рассчитывать: последняя версия вышла в 2006 году.

...или библиотеку Mime-Util:

import eu.medsea.mimeutil.MimeUtil;

public class Main {
  public static void main(String[] args) {
    MimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector");
    File f = new File ("c:/temp/mime/test.doc");
    Collection<?> mimeTypes = MimeUtil.getMimeTypes(f);
    System.out.println(mimeTypes);
    // output : application/msword
  }
}
Минусы: несколько больший размер, есть проблема с "узнаванием" текстовых файлов: все они (включая html) воспринимаются как "application/octet-stream". Плюсы: достаточно высокая точность определения типов, всего одна зависимость (SLF4J), более стабильная работа.

Как правило, более "навороченные" библиотеки, распознающие больше форматов соответственно и "тяжелее". Я уверен, что есть ещё достаточно много библиотек, не описанных в статье, вольный перевод которой я тут попытался представить вашему вниманию. Если у кого-нибудь возникнет желание познакомиться с какой-нибудь подробнее, дайте ссылку в комментариях, я с удовольствием посмотрю на неё и опишу результат знакомства.

Изменение языка интерфейса в NetBeans 7.0+

После выхода 7.0 версии этой чудесной среды появилась весьма хорошая русская локализация. Но мне как и большинству программистам, ближе английский язык в IDE. Решив сменить язык интерфейса я полез в настройки, но к сожалению не нашел ни одного намека на данную функцию. После я прошелся по просторам интернета и нашел вот такие варианты решения:

1. Добавить в строку запуска параметр --locale en:US (FAQ)
Пример :  С:\Program Files\NetBeans 7.0.1\bin\netbeans.exe --locale en:US

2. Добавить --locale en_US в параметр netbeans_default_options в файл <netbeans folder>\etc\netbeans.conf
Пример (в файле С:\Program Files\NetBeans 7.0.1\etc\netbeans.conf):
netbeans_default_options=".... --locale en_US"

З.Ы. Данные решения можно применять как в Windows так и в Linux.

вторник, 4 октября 2011 г.

JSch: Работаем с удалённой машиной по ssh из java-приложения

Хочу рассказать об одной интересной находке: библиотеке JSch. Это реализация ssh соединения на java.  Не уникальная, само собой, но из полудесятка пересмотренных мной, именно она понравилась больше всего.
Возможности весьма впечатляют: аворизация по ключам, порт-форвардинг, соединение через прокси и ещё много чего. Также обрадовала простота работы с библиотекой. Вот, к примеру, реализация удалённой консоли в три десятка строк:

import com.jcraft.jsch.*;
import javax.swing.*;

public class Shell {

  public static void main(String[] arg) {
    try {
      JSch jsch = new JSch();
      String connstr = null;
      if (arg.length > 0) {
        connstr = arg[0];
      } else {
        connstr = JOptionPane.showInputDialog("Enter username:password@hostname", System.getProperty("user.name")+":<pass>"+"@localhost");
      }
      String user = connstr.substring(0, connstr.indexOf(':'));
      String pass = connstr.substring(connstr.indexOf(':')+1, connstr.indexOf('@'));
      String host = connstr.substring(connstr.indexOf('@') + 1);
      Session session = jsch.getSession(user, host, 22);
      session.setPassword(pass);
      session.setConfig("StrictHostKeyChecking", "no");
      session.connect();
      Channel channel = session.openChannel("shell");
      channel.setInputStream(System.in);
      channel.setOutputStream(System.out);
      channel.connect();
    } catch (Exception e) {
      System.out.println(e);
    }
  }
}

Тут мы делаем следующее:
  • Получаем от пользователя настройки подключения (как параметр вызова или диалоговым окном)
  • Создаём сессию и устанавливаем соединение
  • Создаём Channel (в данном случае для интерактивного исполнения команд, хотя есть и другие варианты)
  • "Заворачиваем" системные потоки ввода и вывода в полученный канал
  • наслаждаемся :)

понедельник, 3 октября 2011 г.

Упаковка и распаковка архива на Java

Java без посторонних библиотек поддерживает работу с несколькими видами архивов, в том числе и с самым популярным - Zip. Для этого используются классы из пакета java.util.zip. Давайте посмотрим, как с помощью этих инструментов можно упаковать и распаковать набор каталог с файлами и каталогами.
Упаковка:

  public static void pack(File directory, String to) throws IOException {
    URI base = directory.toURI();
    Deque<File> queue = new LinkedList<File>();
    queue.push(directory);
    OutputStream out = new FileOutputStream(new File(to));
    Closeable res = out;

    try {
      ZipOutputStream zout = new ZipOutputStream(out);
      res = zout;
      while (!queue.isEmpty()) {
        directory = queue.pop();
        for (File child : directory.listFiles()) {
          String name = base.relativize(child.toURI()).getPath();
          if (child.isDirectory()) {
            queue.push(child);
            name = name.endsWith("/") ? name : name + "/";
            zout.putNextEntry(new ZipEntry(name));
          } else {
            zout.putNextEntry(new ZipEntry(name));
            InputStream in = new FileInputStream(child);
            try {
              byte[] buffer = new byte[1024];
              while (true) {
                int readCount = in.read(buffer);
                if (readCount < 0) {
                  break;
                }
                zout.write(buffer, 0, readCount);
              }
            } finally {
              in.close();
            }
            zout.closeEntry();
          }
        }
      }
    } finally {
      res.close();
    }
  }



Этот метод интересен тем, что, в отличие от большинства методов для манипуляции с файлами, он нерекурсивный. Тут для обработки вложенных директорий используется очередь. Кстати, метод не мой, взят отсюда.
Обратный процесс также реализуем нерекурсивным методом.Проходим по всем entry в архиве, директории сразу создаём, файлы добавляем в очередь. Потом проходим по очереди и создаём файлы, копируя их из ZipInputStream в FileOutputStream:

  public static void unpack(String path, String dir_to) throws IOException {
    ZipFile zip = new
ZipFile(path);
    Enumeration entries = zip.entries();
    LinkedList<ZipEntry> zfiles = new LinkedList<ZipEntry>();
    while (entries.hasMoreElements()) {
      ZipEntry entry = (ZipEntry) entries.nextElement();
      if (entry.isDirectory()) {
        new File(dir_to+"/"+entry.getName()).mkdir();
      } else {
        zfiles.add(entry);
      }
    }
    for (ZipEntry entry : zfiles) {
      InputStream in = zip.getInputStream(entry);
      OutputStream out = new FileOutputStream(dir_to+"/"+entry.getName());
      byte[] buffer = new byte[1024];
      int len;
      while ((len = in.read(buffer)) >= 0)
        out.write(buffer, 0, len);
      in.close();
      out.close();
      }
    zip.close();
  }

вторник, 27 сентября 2011 г.

Операции с файлами в Java

На днях делал небольшой файловый менеджер и пришлось погрузиться в манипуляции с файлами. Задача в принципе тривиальная, если не работать с директориями или использовать Apache Commons IO. Но для проекта "весом" в 60 Kb использовать 155 Kb библиотеку для вызова трёх методов... Не наш это путь, верно? Сделаем всё сами.
Начнём с удаления файлов:

  public static void delete(String path_from) {
    File f = new File(path_from);
    if (f.isDirectory()) {
      String[] child = f.list();
      for (int i = 0; i < child.length; i++) {
        delete(path_from + "/" + child[i]);
      }
      f.delete();
    } else {
      f.delete();
    }
  }



Это рекурсивный метод: Он вызывает сам себя для каждого вложенного файла или директории, а затем удаляет и саму директорию. файл он удаляет сразу.
Копирование реализуется ненамного сложнее:

  public static void copy(String from, String to) throws FileNotFoundException, IOException {
    String fname = from.substring(from.lastIndexOf("/") + 1);
    File f = new File(from);
    if (f.isDirectory()) {
      if (new File(to + "/" + fname).exists()) {
        throw new IOException("Directory " + to + "/" + fname + " already exists");
      }
      mkdir(to + "/" + fname);
      String[] child = f.list();
      for (int i = 0; i < child.length; i++) {
        copy(from + "/" + child[i], to + "/" + fname);
      }
    } else {
      if (new File(to + "/" + fname).exists()) {
        throw new IOException("File " + to + "/" + fname + " already exists");
      }
      FileChannel srcChannel = new FileInputStream(f).getChannel();
      FileChannel destChannel = new FileOutputStream(to + "/" + fname).getChannel();
      srcChannel.transferTo(0, srcChannel.size(), destChannel);
    }
  }


Тут добавлены проверки на существование файла и директории перед их созданием. Копирование файла через FileChannel предпочтительнее ибо быстрее.

пятница, 26 августа 2011 г.

Android: делаем холст для рисования

Допустим вы решили сделать маленький Paint для Android. Например, для развития художественных талантов у своего ребёнка :). Основой такого приложения будет холст: некая View, которая может "ощущать" прикосновения и отображать на себе путь пальца. Для реализации таких функций создаём класс, расширяющий View и переопределяем у него два метода: onTouchEvent (позволит перехватывать движения пальца) и onDraw (позволит рисовать).

  1. public class PaintView extends View {
  2.  
  3.   
  4.   private int linecolor, alpha, lwidth;
  5.   private int WIDTH;
  6.   private int HEIGHT;
  7.   MainActivity act;
  8.  
  9.   public PaintView(MainActivity act, int linecolor, int alpha, int lwidth, int width, int height) {
  10.     super(act);
  11.     this.act = act;
  12.     this.linecolor = linecolor;
  13.     this.alpha = alpha;
  14.     this.WIDTH = width;
  15.     this.HEIGHT = height;
  16.     this.lwidth = lwidth;
  17.   }
  18.   
  19.   @Override
  20.   public boolean onTouchEvent(MotionEvent e) {
  21.  
  22.     int action = e.getAction();
  23.  
  24.     if (action == MotionEvent.ACTION_UP) {
  25.       act.addPoint(null);
  26.  
  27.     } else if (action == MotionEvent.ACTION_MOVE || action == MotionEvent.ACTION_DOWN) {
  28.       Point point = new Point(Math.round(e.getX()), Math.round(e.getY()), linecolor, alpha, lwidth);
  29.       act.addPoint(point);
  30.       postInvalidate();
  31.     }
  32.  
  33.     return true;
  34.   }
  35.  
  36.   @Override
  37.   protected void onDraw(Canvas c) {
  38.     Point curr = null;
  39.     for (Point data : act.getPoints()) {
  40.       if (curr != null && data != null) {
  41.         Paint paint = new Paint();
  42.         paint.setColor(data.getColor());
  43.         paint.setAlpha(data.getAlpha());
  44.         if (data.getWidth()==1) {
  45.           c.drawLine(curr.getX(), curr.getY(), data.getX(), data.getY(), paint);
  46.         } else {
  47.           c.drawRect(curr.getX(), curr.getY(), data.getX()+data.getWidth(), data.getY()+data.getWidth(), paint);
  48.         }
  49.       }
  50.       curr = data;
  51.     }
  52.   }
  53.  
  54.   @Override
  55.   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  56.     super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  57.     this.setMeasuredDimension(WIDTH, HEIGHT);
  58.   }
  59. }
Рассмотрим подробнее метод onTouchEvent. Для событий прикосновения и перемещения мы получаем координаты точки на экране и создаём экземпляр Point, добавляя его в ArrayList с помощью метода addPoint(point) в MainActivity. Почему не хранить коллекцию точек прямо в классе? Так проще обрабатывать события, требующие перерисовки холста. Просто создаём новый холст, а данные для отрисовки он возьмёт в MainActivity.
Класс Point выглядит просто:
public class Point {
  private int x, y, color, alpha, width;

  public Point(int x, int y, int color, int alpha, int width) {
    this.x = x;
    this.y = y;
    this.color = color;
    this.alpha = alpha;
    this.width = width;
  }

  public int getX() {
    return x;
  }

  public int getY() {
    return y;
  }

  public int getColor() {
    return color;
  }

  public int getAlpha() {
    return alpha;
  }

  public int getWidth() {
    return width;
  }
}

...это обычное хранилище координат точки и её характеристик.
Вызываем наш холст из MainActivity следующим образом:
PaintView paint = new PaintView(this, initColor, alpha, linew, w, h);
Тут:
this - экземпляр MainActivity, из которого, собственно делаем вызов
initColor, alpha, linew - настройки цвета, прозрачности и толщины линии
w и h - ширина и высота доступной для рисования области в пикселях. Получаем её так:
    Resources res = getResources();
    int h = res.getDisplayMetrics().heightPixels;
    int w = res.getDisplayMetrics().widthPixels;

Созданный экземпляр холста может быть добавлен в нашу структуру View с помощью метода addView родительского контейнера а контейнер верхнего уровня устанавливаем как контент окна методом setContentView.
Останется только добавить в наше приложение панель с кнопками для выбора цвета, толщины линий и т.п. и простенькая "рисовалка" готова :).