четверг, 26 июня 2008 г.

Простейший мидлет с возможностью работы с HTTP

Задача: написать мидлет, который будет посылать некоторый POST-запрос на сервер и выводить ответ.

Аппаратное обеспечение

Проверялось на Sony Ericsson k320i и на Nokia 2600 classic.

Сервер

На сервере у нас простое Web-приложение, написанное на ASP.NET. Для этого приложения напишем хэндлер Handler.ashx, который будет принимать и обрабатывать запросы. К примеру, он будет получать значение переменной message, и возвращать строку "Message is <значение>"
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class Handler : IHttpHandler
{
  public void ProcessRequest(HttpContext context)
  {
    string msg = context.Request["message"];
    context.Response.ContentType = "text/plain";
    context.Response.Write(String.Format("Message is: {0}", msg));
  }

  public bool IsReusable
  {
    get
    {
      return false;
    }
  }
}

Создание мидлета

Для работы использовалась среда NetBeans 6.1. Создаем Mobile Application с профилем MIDP 2.0 (c 2.1 на указанных телефонах мидлет не заработал). На форму мидлета добавим StringItem и назовем MessageString. Также добавим на форму команду OkCommand, и в ее обработчик (не знаю, как это по-явовски правильно называется) добавим вызов функции CallServer Сама функция выглядит примерно таким образом:
public void CallServer()
{
  Runnable r = new Runnable()
  {
    public void run()
    {
      HttpConnection conn = null;
      InputStream is = null;
      OutputStream os = null;
      try
      {
        conn = (HttpConnection) Connector.open("http://localhost:1988/Handler.ashx");

        // Настройки соединения
        conn.setRequestMethod(HttpConnection.POST);
        conn.setRequestProperty("User-Agent", "Profile/MIDP-2.0 Configuration/CLDC-1.0");
        conn.setRequestProperty("Content-Language", "en-US");
        conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

        // GenerateString - некоторая функция, возвращающая строку
        String str = "message=" + GenerateString() + "\r\n";

        // Посылка сообщения
        os = conn.openOutputStream();
        os.write(str.getBytes());
        //os.flush();

        // Смотрим, какой код пришел с сервера
        int rc = conn.getResponseCode();
        if (rc != HttpConnection.HTTP_OK)
        {
          throw new IOException("HTTP responce code: " + rc);
        }                 

        // Получение ответа и вывод его в строке MessageString
        is = conn.openInputStream();
        int ch;
        StringBuffer data = new StringBuffer();
        while ((ch = is.read()) != -1)
        {
          data.append((char) ch);
        }
        MessageString.setText(data.toString());
      }
      catch (Exception ex)
      {
        MessageString.setText("something failed: " + ex.getMessage());
      }
      finally
      {
          // Закрытие соединений
        try
        {
          if (is != null)
          {
            is.close();
          }
          if (os != null)
          {
            os.close();
          }
          if (conn != null)
          {
            conn.close();
          }
        }
        catch (Exception e)
        {
        }
      }
    }
  };
  new Thread(r).start();
}
При работе с HttpConnection обязательно нужно создавать отдельный поток (Runnable), потому что иначе при установке соединения будет возникать дедлок. А возникает он по следующей причине. Поток, инициирующий соединение, блокируются до тех пор, пока соединение не будет установлено. То есть, если код выполняется в системном потоке мидлета, то блокируется системный поток. И тут мидлет вдруг решает спросить у пользователя разрешения на выполнение операции соединения. Без разрешения соединение не установится, а разрешить не получится, потому что системный поток заблокирован. Отсюда дедлок. Статья по этому поводу

четверг, 12 июня 2008 г.

Коллекции в конфиге приложения

Задача была следующая. Нужно было написать робота, который будет ходить по тестовому сайту, делать там хиты, прикидываться разными пользователями, делать несколько сессий, да еще и работать на нескольких браузерах. Робот реализован в виде консольного приложения, для работы с браузерами используется Selenium, параметры (количество пользователей, сессий и хитов, а также список браузеров) хранятся в App.config. Собственно, здесь я напишу о том, как работать с параметрами из конфига.

Добавление в конфиг своей секции

Итак, мы хотим, чтобы в конфиге можно было написать так:
<VisitorSettings hitsPerDay="300" sessionsPerDay="21" duniqsPerDay="15"><Browsers>
  <browser name="chrome" />
  <browser name="iexplore"/>
  <browser name="opera" path="C:\Program Files\Opera\Opera.exe" />
  <browser name="custom" path="C:\Program Files\Safari\Safari.exe" /></Browsers>
</VisitorSettings>
Пишем следующие классы:

Класс для хранения конфигурации браузера

public class Browser : ConfigurationSection
{
  public const string SectionName = "Browsers";
  private const string name = "name";
  private const string path = "path";

  [ConfigurationProperty(name, IsRequired = true)]
  public string Name
  {
    get { return (string)base[name]; }
    set { base[name] = value; }
  }

  [ConfigurationProperty(path, IsRequired = false)]
  public string Path
  {
    get { return (string)base[path]; }
    set { base[path] = value; }
  }

  public string GetBrowserString()
  {
    return String.Format(
      "*{0}{1}{2}",
      Name,
      String.IsNullOrEmpty(Path) ? String.Empty : " ",
      Path
    );
  }
}

Класс для хранения конфигурации списка браузеров

[ConfigurationCollection(typeof(Browser), CollectionType = ConfigurationElementCollectionType.AddRemoveClearMap)]
public class BrowserElementCollection : ConfigurationElementCollection
{
  #region Constructor
  static BrowserElementCollection()
  {
    browsers = new ConfigurationPropertyCollection();
  }
  #endregion

  #region Fields
  private static ConfigurationPropertyCollection browsers;
  #endregion

  #region Properties
  protected override ConfigurationPropertyCollection Properties
  {
    get { return browsers; }
  }

  public override ConfigurationElementCollectionType CollectionType
  {
    get { return ConfigurationElementCollectionType.BasicMap; }
  }

  protected override string ElementName
  {
    get
    {
      return "browser";
    }
  }
  #endregion

  #region Indexers
  public Browser this[int index]
  {
    get { return (Browser)base.BaseGet(index); }
    set
    {
      if (base.BaseGet(index) != null)
      {
        base.BaseRemoveAt(index);
      }
      base.BaseAdd(index, value);
    }
  }
  #endregion

  #region Overrides
  protected override ConfigurationElement CreateNewElement()
  {
    return new Browser();
  }

  protected override object GetElementKey(ConfigurationElement element)
  {
    return (element as Browser).GetBrowserString();
  }
  #endregion
}

Класс для самой секции конфига:

public class VisitorSettings : ConfigurationSection
{
  static public string SectionName = "VisitorSettings";

  const string hitsPerDay = "hitsPerDay";
  const string duniqsPerDay = "duniqsPerDay";
  const string sessionsPerDay = "sessionsPerDay";
  const string browsers = "Browsers";

  [ConfigurationProperty(hitsPerDay, IsRequired = true)]
  public int HitsPerDay
  {
    get { return (int)base[hitsPerDay]; }
    set { base[hitsPerDay] = value; }
  }

  [ConfigurationProperty(sessionsPerDay, IsRequired = true)]
  public int SessionsPerDay
  {
    get { return (int)base[sessionsPerDay]; }
    set { base[sessionsPerDay] = value; }
  }

  [ConfigurationProperty(duniqsPerDay, IsRequired = true)]
  public int DuniqsPerDay
  {
    get { return (int)base[duniqsPerDay]; }
    set { base[duniqsPerDay] = value; }
  }
  [ConfigurationProperty(browsers, IsRequired = true)]
  public BrowserElementCollection Browsers
  {
    get { return (BrowserElementCollection)base[browsers]; }
    set { base[browsers] = value; }
  }
}
Готово! Теперь для полного счастья в App.config configSections осталось прописать:
<section name="VisitorSettings" type="Visitor.VisitorSettings, Visitor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />

Чтение данных из конфига

Теперь можно писать так:
int uniques = WatcherConfiguration.VisitorSettings.DuniqsPerDay;
int sessions = WatcherConfiguration.VisitorSettings.SessionsPerDay;
int hits = WatcherConfiguration.VisitorSettings.HitsPerDay;
BrowserElementCollection browsers = WatcherConfiguration.VisitorSettings.Browsers;

Редактирование конфига из программы

Пусть теперь у нас есть админка, из которой мы должны иметь возможность добавлять и удалять браузеры. Сначала нужно добавить в класс BrowserElementCollection следующие методы:
#region Методы
public void Add(Browser browser)
{
  base.BaseAdd(browser);
}

public void Remove(string key)
{
  base.BaseRemove(key);
}

public void Remove(Browser browser)
{
  base.BaseRemove(GetElementKey(browser));
}

public void Clear()
{
  base.BaseClear();
}

public void RemoveAt(int index)
{
  base.BaseRemoveAt(index);
}

public string GetKey(int index)
{
  return (string)base.BaseGetKey(index);
}
#endregion
Теперь добавлять браузеры мы можем так:
protected void AddBrowser(string name, string path)
{
  Browser browser = new Browser()
  {
    Name = name;
    Path = path;
  };

  WatcherConfiguration.VisitorSettings.Browsers.Add(browser);
  WatcherConfiguration.SaveAll();
}
А удалять - так:
protected void AddBrowser(string name, string path)
{
  WatcherConfiguration.VisitorSettings.Browsers.RemoveAt(index);
  WatcherConfiguration.SaveAll();
}

понедельник, 2 июня 2008 г.

Отключение internal log в log4net

log4net имеет обыкновение выводить в консоль еще и свой собственный вывод. Выглядит это примерно так:
log4net: XmlHierarchyConfigurator: Configuration update mode [Merge].
log4net: XmlHierarchyConfigurator: Logger [root] Level string is [ALL].
log4net: XmlHierarchyConfigurator: Logger [root] level set to [name="ALL",value=-2147483648].
...
Называется это internal log. Когда мы пишем web-приложение, мы его не видим и не знаем. Но стоит начать писать консольное приложение, использующие log4net, как он тут же вылезает и начинает мешать. Для того, чтобы отключить, достаточно в секции appSettings конфигурационного файла приложения написать:
<add key="log4net.Internal.Quiet" value="true" />