Author Archive

Подбор окончания в соответствии с количеством

Шлифуя очередную программу и вкладывая в нее души, хочется выводить не: "1 карточек", "2 карточек" и "5 карточек", а "1 карточка", "2 карточки" и 5 "карточек". Т.е. к слову, связанному с целым количественным числительным хочется подобрать правильное окончание. Можно заменить, что для целых количественных числительных максимально можно выделить три различных окончания. Эти рассуждения привели меня к написанию следующей функции:
public static String pickPhrase(int count, String word0,String word1,String word2) {
    int rem = count % 100;
    if(rem < 11 || rem > 14){
        rem = count % 10;
        if(rem == 1) return word1;
        if(rem >= 2 && rem <= 4) return word2;
    } return word0;
}
Функция pickPhrase принимает количество (count) и три вида окончаний (word0, word1, word2). Мысленно можно выделить следующие группы: 0 карточек, 5 карточек, 6 карточек и т.д. 1 карточка, 21 карточка, 31 карточка и т.д. 2 карточки, 3 карточки, 4 карточки и т.д. Для удобства я взял окончания для 0, 1, 2, т.к. у них троих различные окончания. Вам только остается подобрать окончания для 0, 1, 2, а функция по переданную количеству подберет требуемое окончание. word0 - соответствует окончанию для 0-го количества; word1 - соответствует окончанию с количеством в 1 штуку; word2 - соответствует окончанию с количеством в 2 штуки. Например: Пусть дано кол-во карточек от 0 до 100 включительно и нужно к слову "карточка" подобрать окончание в соответствии с количеством. Это делается так:
for (int i = 0; i <= 100; i++) {
    System.out.println(i + " карточ" + pickPhrase(i, "ек", "ка", "ки"));
}
Мы выделили неизменяемую часть слова "карточ" и далее нашей функцией подобрали окончание.
0 карточек
1 карточка
2 карточки
3 карточки
4 карточки
5 карточек
6 карточек
7 карточек
8 карточек
9 карточек
10 карточек
11 карточек
12 карточек
...
96 карточек
97 карточек
98 карточек
99 карточек
100 карточек
Чтобы не выделить неизменяемую часть слова и не допустить ошибку при этом, можно вместо окончаний использовать сами слова. Например:
for (int i = 0; i <= 100; i++) {
    System.out.println(i + " " + pickPhrase(i, "штук", "штука", "штуки"));
}
0 штук
1 штука
2 штуки
3 штуки
4 штуки
5 штук
6 штук
7 штук
8 штук
9 штук
10 штук
11 штук
12 штук
13 штук
...
90 штук
91 штука
92 штуки
93 штуки
94 штуки
95 штук
96 штук
97 штук
98 штук
99 штук
100 штук

Парсинг DN (Distinguished Names)

В ЭЦП данные о её владельце и изготовителе хранятся в виде Distinguished Names (DN). Например:
CN=БЛОГОВ БЛОГ,SURNAME=БЛОГОВ,SERIALNUMBER=IIN123456789012,C=KZ,L=АСТАНА,S=АСТАНА,G=БЛОГОВИЧ,O=ТОО \"Рога\, копыта 24\\7\",OU=BIN000111222333
Атрибуты отделяются между собой запятыми, каждый атрибут представляет из себя ключ и значение, разделенные символом равно (=). Так же некоторые символы экранируются, например: \" \, Предлагаю следующую функцию, которая принимает строку в формате DN и возвращается объект с соответствующими полями и значениями (я использовал angular'овский цикл, но можно заменить на обычный).
function parseDN(value){
    var parts = value
        .replace(/\\,/g,'')
        .replace(/,/g,'')
        .replace(//g,',')
        .replace(/\\\\/g,'')
        .replace(/\\/g,'')
        .replace(//g,'\\')
        .split('');
    var map = {};
    angular.forEach(parts, function(part){
        var split = part.split('=');
        map[split[0]] = split[1];
    });
    return map;
}
Пример использования:
    var map = parseDN("YOUR_DN_STRING");
    console.log(map.CN);
    console.log(map.SERIALNUMBER);
    console.log(map.O);

Интеграция Discourse с Keycloak

Статья подразумевает, что у вас уже установлены и настроены Discourse c Keycloak. Интегрировать Discourse с Keycloak будем через плагин discourse-oauth2-basic. Для этого нужно папку плагина discourse-oauth2-basic перенести в директорию plugins, которая находится в корневой папке Discourse. После запуска Discourse c новым плагином, который автоматически подтянется из директории plugins, в админке появятся новые поля настроек. Их можно найти по ссылке http://localhost:3000/admin/site_settings/category/login, либо найти самостоятельно по пути: Админка -> Настройки -> Учетные записи. Новые появившиеся поля нужно заполнить в соответствиями с настройками вашего клиента в Keycloak:
Название параметра Значение параметра
oauth2 enabled
oauth2 client id discourse-client
oauth2 client secret 3b0eab79-34df-4fd3-861e-6654f3bc4c13
oauth2 authorize url http://localhost:18100/auth/realms/ltms/protocol/openid-connect/auth
oauth2 token url http://localhost:18100/auth/realms/ltms/protocol/openid-connect/token
oauth2 user json url http://localhost:18100/auth/realms/ltms/protocol/openid-connect/userinfo
oauth2 json user id path sub
oauth2 json username path name
oauth2 json name path name
oauth2 json email path email
oauth2 email verified
oauth2 send auth header
oauth2 button title with OAuth2
Применив указанные выше настройки, в интерфейсе аутентификации появится кнопка для авторизации через OAuth2. discourse-with-aouth2 После нажатия кнопки with OAuth2 (название можно поменять в настройках) открывается окно аутентификации Keycloak. keycloak-auth Успешно залогинившись, вы возвращаетесь в Discourse, который теперь знает о вашем присутствии 🙂

Подключение MongoDB(+GridFS) к Play2 (Scala)

В нашем новом проекте в качестве файлового хранилища мы решили использовать GridFS. GridFS эффективен для файлов размером свыше 16 МБ, поэтому мы решили хранить файлы менее 16 МБ в виде байтов в MongoDB. К тому же GridFS - это всего лишь надстройка над MongoDB, и размер документа MongoDB ограничен как раз 16 мегабайтами. Для начала нужно в build.sbt добавить зависимость на стандартный драйвер MongoDB:
"org.mongodb" % "mongo-java-driver" % "3.3.0"
Далее в application.conf прописать переменные, который мы будем использовать для подключения к MongoDB.
mongodb.uri="mongodb://192.168.99.100:27017"
mongodb.db="your_database_name"
И, собственно, реализация класса для работы с MongoDB и GridFS:
package services.file
import javax.inject.{Inject, Singleton}
import com.mongodb.client.gridfs.{GridFSBuckets, GridFSBucket}
import com.mongodb.{MongoClient, MongoClientURI}
import play.api.{Logger, Configuration}

@Singleton
class MongoService @Inject() (conf: Configuration) {
  val uri = conf.underlying.getString("mongodb.uri")
  val db = conf.underlying.getString("mongodb.db")
  Logger.debug("mongodb.uri = " + uri)
  Logger.debug("mongodb.db = " + db)
  val mongoClient = new MongoClient(new MongoClientURI(uri))
  val database = mongoClient.getDatabase(db)
  val wholeFileCollection = database.getCollection("whole-files")

  //GridFS
  val gridFSBucket: GridFSBucket = GridFSBuckets.create(database, "chunked-files")
}

Тестовые ссылки с различными файлами

Часто бывает необходимо проверить какой-то функционал по получению файла через web. Мы либо ищем в интернете нужную ссылку, либо размещаем файлы где-то у себя на веб-сервере (который еще нужно поднять :-)). Сегодня пришла идея сделать страницу, на которой будут размешаться ссылки на тестовые файлы, причем файлы будут храниться на этом же домене, поэтому пока работает этот домен, файлы будут доступны!
Наименование файла Ссылка Размер MD5
fontan.mp4 http://kesh.kz/download/test/fontan.mp4 62080018 байт (59.2M) ee873149a67990babe3206c5d06356eb
Гимн Казахстана.txt http://kesh.kz/download/test/%D0%93%D0%B8%D0%BC%D0%BD%20%D0%9A%D0%B0%D0%B7%D0%B0%D1%85%D1%81%D1%82%D0%B0%D0%BD%D0%B0.txt 975 байт edbf47380c466f267511a6893e954693

Конвертирование кортежа со списком в JSON (Scala)

Всем привет! Меня перевели на новый проект, решили делать его на Play2(Scala). Я джавист и тяжело с моим багажом перестраиваться, прежние подходы в разработке не работают. Долго бился на конвертированием структуры в виде кортежа со списком (List). В итоге пришел к такому варианту:
implicit val questionReads = Json.reads[Question]
implicit val questionWrites = Json.writes[Question]
implicit val answerWrites = Json.writes[Answer]
implicit val answerReads = Json.reads[Answer]

implicit val questionWithAnswersWrites = new Writes[(Option[Question], List[Answer])] {
    override def writes(tuple: (Option[Question], List[Answer])) = Json.obj(
      "question" -> tuple._1,
      "answers" -> tuple._2
    )
  }

implicit val questionWithAnswerReads = new Reads[(Question, List[Answer])] {
    override def reads(json: JsValue) = JsSuccess((
      (json \ "question").as[Question],
      (json \ "answers").as[List[Answer]]
    ))
  }

def questionWithAnswers(id: Long) = Action {
    val answers: (Option[Question], List[Answer]) = questionService.questionWithAnswers(id)
    Ok(Json.toJson(answers))
  }

def saveWithAnswers = Action(BodyParsers.parse.json) { implicit request =>
    request.body.validate[(Question,List[Answer])].fold(
      error => {
        BadRequest(Json.obj("status" -> JsError.toJson(error)))
      },
      tuple => {
        questionService.update(tuple._1,tuple._2)
        Ok(Json.obj("status" -> "success"))
      }
    )
  }
Question и Answer - это case class'ы

Встраивание атрибута Id в тег Body SOAP-запроса

С некоторой версии Java (примерно с 7u25 см. http://stackoverflow.com/questions/17331187/xml-dig-sig-error-after-upgrade-to-java7u25) начались проблемы со встаиванием атрибута Id в тег Body SOAP-запроса. Такое встраивание необходимо с целью подписания запроса ЭЦП. Пока я встретил две ошибки в зависимости каким путем идет встраивание атрибута Id:
  1. com.sun.org.apache.xml.internal.security.utils.resolver.implementations.ResolverFragment
  2. org.apache.xml.security.utils.resolver.ResourceResolverException: Cannot resolve element with ID ...
Чтобы решить проблему нужно специальным образом пометить атрибут Id как ID (простите за тавтологию) Первая ошибка возникает в коде такого вида:
Attr attr = doc.createAttribute("id");
attr.setValue("body");
body.getAttributes().setNamedItem(attr);
Подкорректированная версия:
Attr attr = doc.createAttribute("id");
attr.setValue("body");
body.getAttributes().setNamedItem(attr);
((Element) body).setIdAttributeNode(attr, true);//fix
Вторая ошибка возникает в коде вида:
SOAPBody body = env.getBody();
body.addAttribute(new QName("Id"), id);
Подкорректированная версия:
SOAPBody body = env.getBody();
body.addAttribute(new QName("Id"), id);
body.setIdAttribute("Id", true);//fix
Если помог отпишитесь 🙂

Бэкапирование

В прежние времена я часто занимался автоматизацией бэкапирования, т.е. создания резервной копии баз данных, файлов и т.д.. Сейчас таких задач у меня не возникает, но все же решил написать о том, как писать такие скрипты. Перечислим базовые шаги алгоритма скрипта:
  1. Выбрать директорию, где храняться резервные копии
  2. Снять резервную копию базы данных
  3. Сжать файлы с помощью архиватора и присвоить удобное наименование архиву
  4. Добавить скрипт в планировщик и установить время и периодичность запуска
Рассмотрим версии скрипта для Windows и для Linux.

Нахождение медианы в массиве (MySQL)

Нахождение медианы в массиве для MySQL я решил способом ниже. Кто знает более лучшие варианты скидывайте :). Но, желательно, без использования стандартных функций СУБД для нахождения медианы.
set @a := 0;
set @cnt := (select count(*) from t_table);

select 
    (sum(case when num = ceil(@cnt / 2) then c_value else null end)
   + sum(case when num = ceil((@cnt + 1) / 2) then c_value else null end)) / 2
from(
    select (@a := @a + 1) num,c_value from t_table order by c_value
)w

Как искать ошибку в программе?

Список подсказок, как найти ошибку:
  1. Проверить, там ли вы ищите/смотрите ошибку. Например, вы могли подключиться не к тому серверу или базе.
  2. Вспомнить и посмотреть, что недавно менялось.
  3. Попросить коллегу посмотреть на проблему незамыленным взглядом
  4. Отстраниться от проблемы. Например, переключиться на другую сферу деятельности, проблему или просто отдохнуть
  5. Внимательно посмотреть сообщение об ошибке, может там уже указана точная причина ошибки или хотя бы указатель на нее
  6. Поискать способ (если возможно) просмотра детализованного лога
  7. Вернуться к первому пункту 🙂