/* * Overchan Android (Meta Imageboard Client) * Copyright (C) 2014-2016 miku-nyan <https://github.com/miku-nyan> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package nya.miku.wishmaster.api.util; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import nya.miku.wishmaster.api.models.AttachmentModel; import nya.miku.wishmaster.api.models.BadgeIconModel; import nya.miku.wishmaster.api.models.PostModel; import nya.miku.wishmaster.api.models.SimpleBoardModel; import nya.miku.wishmaster.api.models.UrlPageModel; import nya.miku.wishmaster.cache.SerializablePage; public class ChanModels { private static final String NULL_KEY = null; private static final String SEPARATOR = "---"; /** * Рассчитать хэш для модели адреса страницы * @param model модель * @return строка хэша * @throws IllegalArgumentException если данная модель не является адресом страницы АИБ с постами или тредами * (т.е. страница не представима в {@link SerializablePage}) */ public static String hashUrlPageModel(UrlPageModel model) throws IllegalArgumentException { if (model.type == UrlPageModel.TYPE_OTHERPAGE) { throw new IllegalArgumentException("cannot compute hash for non-board page"); } StringBuilder key = new StringBuilder(model.chanName); if (model.type != UrlPageModel.TYPE_INDEXPAGE) { key.append(SEPARATOR).append(model.boardName).append(SEPARATOR); } switch (model.type) { case UrlPageModel.TYPE_BOARDPAGE: key.append("boardPage").append(model.boardPage); break; case UrlPageModel.TYPE_CATALOGPAGE: key.append("catalogType").append(model.catalogType); break; case UrlPageModel.TYPE_THREADPAGE: key.append("threadNumber").append(model.threadNumber); break; case UrlPageModel.TYPE_SEARCHPAGE: key.append("searchRequest").append(model.searchRequest); break; } return CryptoUtils.computeMD5(key.toString()); } /** * Рассчитать хэш для модели поста. Параметр {@link PostModel#deleted} не влияет на значение контрольной суммы. * @param m модель * @return контрольная сумма (хэш), целое число */ public static int hashPostModel(PostModel m) { int hash = 1; for (Object o : new Object[] { m.number, m.name, m.subject, m.comment, m.email, m.trip, m.op, m.sage, m.timestamp, m.parentThread }) hash = hash*31 + (o==null ? 0 : o.hashCode()); if (m.icons != null) for (BadgeIconModel icon : m.icons) for (Object o : new Object[] { icon.source, icon.description }) hash = hash*31 + (o==null ? 0 : o.hashCode()); if (m.attachments != null) for (AttachmentModel attach : m.attachments) for (Object o : new Object[] { attach.thumbnail, attach.path, attach.originalName, attach.type, attach.size, attach.isSpoiler }) hash = hash*31 + (o==null ? 0 : o.hashCode()); if (m.color != 0) hash = hash*31 + m.color; return hash; } /** * Определить примерный (может отличаться на разных платформах) размер модели поста (объекта {@link PostModel}) в памяти (Heap) в байтах * @param model модель * @return размер в байтах */ public static int getPostModelSize(PostModel model) { int size = 64; for (String s : new String[] { model.number, model.name, model.subject, model.comment, model.email, model.trip, model.parentThread }) if (s != null) size += (40 + (s.length() * 2)); if (model.attachments != null) { size += (12 + (model.attachments.length * 4)); for (AttachmentModel attachment : model.attachments) if (attachment != null) { size += 48; if (attachment.thumbnail != null) size += (40 + (attachment.thumbnail.length() * 2)); if (attachment.path != null) size += (40 + (attachment.path.length() * 2)); if (attachment.originalName != null) size += (40 + (attachment.originalName.length() * 2)); } } if (model.icons != null) { size += (12 + (model.icons.length * 4)); for (BadgeIconModel icon : model.icons) if (icon != null) { size += 16; if (icon.source != null) size += (40 + (icon.source.length() * 2)); if (icon.description != null) size += (40 + (icon.description.length() * 2)); } } return size; } /** * Рассчитать хэш для модели вложения. Параметр {@link AttachmentModel#isSpoiler} не учитывается. * @param model модель * @return строка хэша */ public static String hashAttachmentModel(AttachmentModel model) { StringBuilder key = new StringBuilder(); for (String s : new String[] { model.thumbnail, model.path, model.originalName }) key.append(s).append(SEPARATOR); key.append(model.type).append(SEPARATOR).append(model.size); return CryptoUtils.computeMD5(key.toString()); } /** * Рассчитать хэш для модели иконки бэйджа (флаг страны, политические предпочтения и т.д.) * @param model модель * @param chanName название АИБ (модуля чана) * @return строка хэша */ public static String hashBadgeIconModel(BadgeIconModel model, String chanName) { StringBuilder key = new StringBuilder(); for (String s : new String[] { chanName, model.source, model.description }) key.append(s).append(SEPARATOR); return CryptoUtils.computeMD5(key.toString()); } /** * Создать упрощённую модель доски ({@link SimpleBoardModel} * @param chan название имиджборды * @param boardName код доски (короткое название) * @param description описание доски (полное название) * @param category категория, к которой относится доска (в случае деления досок по категориям), может принимать null. * @param nsfw должно принимать true, если на доске содержится контент 18+ или доска является немодерируемой */ public static SimpleBoardModel obtainSimpleBoardModel(String chan, String boardName, String description, String category, boolean nsfw) { SimpleBoardModel model = new SimpleBoardModel(); model.chan = chan; model.boardName = boardName; model.boardDescription = description; model.boardCategory = category; model.nsfw = nsfw; return model; } /** * Слияние списков постов, с сохранением удалённых постов (которые присутствовали в старом списке, но отсутствуют в новом).<br> * Сложность O(N) в среднем * @param oldList старый список постов * @param newList новый список постов * @return полученный массив */ public static PostModel[] mergePostsLists(List<PostModel> oldList, List<PostModel> newList) { Set<String> newListNumbers; if (postNumbersSet(oldList) == null || (newListNumbers = postNumbersSet(newList)) == null) return newList.toArray(new PostModel[newList.size()]); Map<String, PostModel> skipped = new HashMap<String, PostModel>(); for (int i=0; i<oldList.size(); ++i) if (!newListNumbers.contains(oldList.get(i).number)) skipped.put(i>0 ? oldList.get(i-1).number : NULL_KEY, oldList.get(i)); if (skipped.isEmpty()) return newList.toArray(new PostModel[newList.size()]); List<PostModel> result = new ArrayList<PostModel>(newList.size() + skipped.size()); if (skipped.containsKey(NULL_KEY)) { PostModel toResult = skipped.get(NULL_KEY); toResult.deleted = true; result.add(toResult); while (skipped.containsKey(toResult.number)) { toResult = skipped.get(toResult.number); toResult.deleted = true; result.add(toResult); } } for (int i=0; i<newList.size(); ++i) { PostModel toResult = newList.get(i); result.add(toResult); while (skipped.containsKey(toResult.number)) { toResult = skipped.get(toResult.number); toResult.deleted = true; result.add(toResult); } } return result.toArray(new PostModel[result.size()]); } /** * Создать множество идентификаторов ({@link PostModel#number}) постов. * Используется также для проверки наличия дубликатов идентификаторов, если метод вернёт null * @param source коллекция с исходными постами * @return созданное множество или null, если присутствуют дубликаты по значению {@link PostModel#number} * или элементы со значением {@link PostModel#number}, равным null */ private static Set<String> postNumbersSet(Collection<PostModel> source) { Set<String> result = new HashSet<String>(Math.max((int) (source.size()/.75f) + 1, 16), .75f); for (PostModel post : source) { if (post.number == null || result.contains(post.number)) return null; result.add(post.number); } return result; } }