package org.araqne.logstorage.file; import java.io.File; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.ListIterator; import java.util.Locale; import java.util.Map; import java.util.TimeZone; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicLong; import org.apache.felix.ipojo.annotations.Component; import org.apache.felix.ipojo.annotations.Invalidate; import org.apache.felix.ipojo.annotations.Requires; import org.apache.felix.ipojo.annotations.Validate; import org.araqne.confdb.ConfigService; import org.araqne.log.api.TimeZoneMappings; import org.araqne.logstorage.CallbackSet; import org.araqne.logstorage.LogFileService; import org.araqne.logstorage.LogFileServiceRegistry; import org.araqne.logstorage.LogTableRegistry; import org.araqne.logstorage.StorageConfig; import org.araqne.logstorage.TableConfig; import org.araqne.logstorage.TableConfigSpec; import org.araqne.logstorage.TableSchema; import org.araqne.logstorage.engine.ConfigUtil; import org.araqne.logstorage.engine.Constants; import org.araqne.storage.api.FilePath; import org.araqne.storage.api.FilePathNameFilter; import org.araqne.storage.api.StorageManager; import org.araqne.storage.localfile.LocalFilePath; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Component(name = "logstorage-log-file-service-txt", immediate = true) public class LogFileServiceTxt implements LogFileService { private final Logger logger = LoggerFactory.getLogger(LogFileServiceV2.class); private static final String OPT_STORAGE_CONFIG = "storageConfig"; private static final String OPT_TABLE_NAME = "tableName"; private static final String OPT_DAY = "day"; private static final String OPT_BASE_PATH = "basePath"; private static final String OPT_INDEX_PATH = "indexPath"; private static final String OPT_DATA_PATH = "dataPath"; private static final String OPT_KEY_PATH = "keyPath"; private static final Object OPT_CALLBACK_SET = "callbackSet"; private static final Object OPT_LASTKEY = "lastKey"; private FilePath logDir; public static class Option extends TreeMap<String, Object> { private static final long serialVersionUID = 1L; public Option(StorageConfig config, Map<String, String> tableMetadata, String tableName, FilePath basePath, FilePath indexPath, FilePath dataPath, FilePath keyPath) { this.put(OPT_STORAGE_CONFIG, config); this.putAll(tableMetadata); this.put(OPT_TABLE_NAME, tableName); this.put(OPT_BASE_PATH, basePath); this.put(OPT_INDEX_PATH, indexPath); this.put(OPT_DATA_PATH, dataPath); this.put(OPT_KEY_PATH, keyPath); } } @Requires private LogTableRegistry tableRegistry; @Requires private LogFileServiceRegistry registry; @Requires private StorageManager storageManager; @Requires private ConfigService conf; @Validate public void start() { logDir = storageManager.resolveFilePath(System.getProperty("araqne.data.dir")).newFilePath("araqne-logstorage/log"); logDir = storageManager.resolveFilePath(getStringParameter(Constants.LogStorageDirectory, logDir.getAbsolutePath())); logDir.mkdirs(); registry.register(this); } private String getStringParameter(Constants key, String defaultValue) { String value = ConfigUtil.get(conf, key); if (value != null) return value; return defaultValue; } @Invalidate public void stop() { if (registry != null) registry.unregister(this); } @Override public String getType() { return "txt"; } @Override public long count(FilePath f) { return 0; } private String retrieveConfig(StorageConfig primaryStorage, String configKey, String defaultValue) { String configValue = defaultValue; TableConfig config = primaryStorage.getConfig(configKey); if (config != null) configValue = config.getValue(); return configValue; } private String convertToRegex(String target) { String regexStr = target; if (target != null && !target.isEmpty()) { regexStr = regexStr.replace(".", "\\."); regexStr = regexStr.replace("*", ".*"); regexStr = regexStr.replace("?", ".?"); } return regexStr; } @Override public List<Date> getPartitions(String tableName) { TableSchema schema = tableRegistry.getTableSchema(tableName, true); StorageConfig primaryStorage = schema.getPrimaryStorage(); String fileNamePrefix = retrieveConfig(primaryStorage, "filename_prefix", ""); String dateFormatString = retrieveConfig(primaryStorage, "date_format", ""); String fileNameSuffix = retrieveConfig(primaryStorage, "filename_suffix", ""); fileNameSuffix = convertToRegex(fileNameSuffix); String dateLocale = retrieveConfig(primaryStorage, "date_locale", "en"); SimpleDateFormat dateFormat = new SimpleDateFormat(dateFormatString, new Locale(dateLocale)); String timeZone = retrieveConfig(primaryStorage, "timezone", ""); if (!timeZone.isEmpty()) { if (TimeZoneMappings.getTimeZone(timeZone) != null) timeZone = (String) TimeZoneMappings.getTimeZone(timeZone); dateFormat.setTimeZone(TimeZone.getTimeZone(timeZone)); } FilePath baseDir = logDir.newFilePath(Integer.toString(schema.getId())); if (schema.getPrimaryStorage().getBasePath() != null) baseDir = storageManager.resolveFilePath(schema.getPrimaryStorage().getBasePath()); FilePath[] filesFilteredByPrefix = filterByPrefix(baseDir, fileNamePrefix); String[] dateFormatsSplitedBySlash = dateFormatString.split("/"); SimpleDateFormat[] splitedDateFormatArr = new SimpleDateFormat[dateFormatsSplitedBySlash.length]; for (int i = 0; i < dateFormatsSplitedBySlash.length; i++) { splitedDateFormatArr[i] = new SimpleDateFormat(dateFormatsSplitedBySlash[i], new Locale(dateLocale)); if (!timeZone.isEmpty()) splitedDateFormatArr[i].setTimeZone(TimeZone.getTimeZone(timeZone)); } int reflectedPathCharCnt = baseDir.getAbsolutePath().length() + fileNamePrefix.length(); boolean isDateFormatEndWithSlash = dateFormatString.endsWith("/"); List<FilePath> filesFilteredByDateFormat = filterByDateFormat(filesFilteredByPrefix, splitedDateFormatArr, 0, reflectedPathCharCnt, isDateFormatEndWithSlash); List<Date> dates = new ArrayList<Date>(); int dateFormatOccurIdx = reflectedPathCharCnt + 1; dates = extractDatesFromFiles(filesFilteredByDateFormat, dateFormatOccurIdx, dateFormat, fileNameSuffix); Collections.sort(dates, Collections.reverseOrder()); return dates; } private FilePath[] filterByPrefix(FilePath baseDir, final String fileNamePrefix) { FilePath[] filesFilteredByPrefix = null; if (fileNamePrefix != null && !fileNamePrefix.isEmpty()) { final int lastSlashInPrefixIdx = fileNamePrefix.lastIndexOf("/"); if (lastSlashInPrefixIdx > 0) baseDir = baseDir.newFilePath(fileNamePrefix.substring(0, lastSlashInPrefixIdx)); filesFilteredByPrefix = baseDir.listFiles(new FilePathNameFilter() { @Override public boolean accept(FilePath dir, String name) { if (fileNamePrefix.length() == lastSlashInPrefixIdx + 1) return true; return name.startsWith(fileNamePrefix.substring(lastSlashInPrefixIdx + 1)); } }); } else { filesFilteredByPrefix = baseDir.listFiles(); } return filesFilteredByPrefix; } private List<FilePath> filterByDateFormat(FilePath[] files, SimpleDateFormat[] splitedDateFormatArr, int splitedDateFormatIdx, int reflectedPathCharCnt, boolean isDateFormatEndWithSlash) { List<FilePath> filteredFiles = new ArrayList<FilePath>(); if (files == null) return filteredFiles; for (FilePath file : files) { String targetName = file.getAbsolutePath().substring(reflectedPathCharCnt + 1); Date dayForCompare; try { dayForCompare = splitedDateFormatArr[splitedDateFormatIdx].parse(targetName); boolean isFullMatched = splitedDateFormatArr[splitedDateFormatIdx].format(dayForCompare).equals(targetName); if (splitedDateFormatArr.length == splitedDateFormatIdx + 1) { if (!isDateFormatEndWithSlash) { filteredFiles.add(file); } else if (isFullMatched) { filteredFiles.addAll(Arrays.asList(file.listFiles())); } else { logger.error("araqne logstorage: invalid log filename, {}", file.getAbsoluteFilePath()); } } else if (file.isDirectory() && splitedDateFormatArr.length > splitedDateFormatIdx + 1) { if (!isFullMatched) { logger.error("araqne logstorage: invalid log filename, {}", file.getAbsoluteFilePath()); continue; } int appendedLen = 1; appendedLen += targetName.length(); filteredFiles.addAll(filterByDateFormat(file.listFiles(), splitedDateFormatArr, splitedDateFormatIdx + 1, reflectedPathCharCnt + appendedLen, isDateFormatEndWithSlash)); } else { logger.error("araqne logstorage: invalid log filename, {}", file.getAbsoluteFilePath()); } } catch (ParseException e1) { logger.error("araqne logstorage: invalid log filename, {}", file.getAbsoluteFilePath()); } } return filteredFiles; } private List<Date> extractDatesFromFiles(List<FilePath> files, int dateFormatOccurIdx, SimpleDateFormat dateFormat, String fileNameSuffix) { List<Date> dates = new ArrayList<Date>(); String[] suffixSplitedBySlash = fileNameSuffix.split("/"); // assign current year to date Calendar yearModifier = null; if (!dateFormat.toPattern().contains("yyyy")) { yearModifier = Calendar.getInstance(); yearModifier.setTimeZone(dateFormat.getTimeZone()); } ListIterator<FilePath> li = files.listIterator(); while (li.hasNext()) { FilePath file = li.next(); String targetName = file.getAbsolutePath().substring(dateFormatOccurIdx); targetName = targetName.replace("\\", "/"); Date d = null; try { d = dateFormat.parse(targetName); if (dates.contains(d)) continue; String dateString = dateFormat.format(d); if (isMatchedWithSuffix(file, suffixSplitedBySlash, 0, dateFormatOccurIdx + dateString.length())) { if (yearModifier != null) { int year = Calendar.getInstance().get(Calendar.YEAR); yearModifier.setTime(d); yearModifier.set(Calendar.YEAR, year); d = yearModifier.getTime(); } dates.add(d); } else { logger.error("araqne logstorage: invalid log filename, {}", file.getAbsoluteFilePath()); } } catch (ParseException e1) { logger.error("araqne logstorage: invalid log filename while exracting {}", file.getAbsoluteFilePath()); } } return dates; } private boolean compareTargetAndSuffix(String target, String suffix) { if (suffix == null || suffix.isEmpty()) { return target == null || target.isEmpty(); } else { return target.matches(suffix); } } private boolean isMatchedWithSuffix(FilePath file, String[] suffixSplitedBySlash, int splitedSuffixIdx, int checkedLength) { boolean isMatched = false; String targetStr = file.getAbsolutePath().substring(checkedLength); if (compareTargetAndSuffix(targetStr, suffixSplitedBySlash[splitedSuffixIdx])) { if (suffixSplitedBySlash.length == splitedSuffixIdx + 1) { return file.isFile(); } else if (file.isDirectory()) { for (FilePath subFile : file.listFiles()) { isMatched = isMatchedWithSuffix(subFile, suffixSplitedBySlash, splitedSuffixIdx + 1, checkedLength + targetStr.length() + 1); if (isMatched) break; } } } return isMatched; } @Override public LogFileWriter newWriter(Map<String, Object> options) { checkOption(options); String tableName = (String) options.get(OPT_TABLE_NAME); Date day = (Date) options.get(OPT_DAY); FilePath indexPath = getFilePath(options, OPT_INDEX_PATH); FilePath dataPath = getFilePath(options, OPT_DATA_PATH); CallbackSet cbSet = (CallbackSet) options.get(OPT_CALLBACK_SET); AtomicLong lastKey = (AtomicLong) options.get(OPT_LASTKEY); try { return new LogFileWriterTxt(indexPath, dataPath, cbSet, tableName, day, lastKey); } catch (Throwable t) { throw new IllegalStateException("cannot open writer txt: data file - " + dataPath.getAbsolutePath(), t); } } private void checkOption(Map<String, Object> options) { for (String key : new String[] { OPT_INDEX_PATH, OPT_DATA_PATH }) { if (!options.containsKey(key)) throw new IllegalArgumentException("LogFileServiceV1: " + key + " must be supplied"); } } private FilePath getFilePath(Map<String, Object> options, String optName) { Object obj = options.get(optName); if (obj == null) return (FilePath) obj; else if (obj instanceof File) { return new LocalFilePath((File) obj); } else { return (FilePath) obj; } } @Override public LogFileReader newReader(String tableName, Map<String, Object> options) { checkOption(options); Date day = (Date) options.get("day"); TableSchema schema = tableRegistry.getTableSchema(tableName, true); StorageConfig primaryStorage = schema.getPrimaryStorage(); String fileNamePrefix = retrieveConfig(primaryStorage, "filename_prefix", ""); String dateFormatString = retrieveConfig(primaryStorage, "date_format", ""); String fileNameSuffix = retrieveConfig(primaryStorage, "filename_suffix", ""); String dateLocale = retrieveConfig(primaryStorage, "date_locale", "en"); SimpleDateFormat dateFormat = new SimpleDateFormat(dateFormatString, new Locale(dateLocale)); String timeZone = retrieveConfig(primaryStorage, "timezone", ""); if (!timeZone.isEmpty()) { if (TimeZoneMappings.getTimeZone(timeZone) != null) timeZone = (String) TimeZoneMappings.getTimeZone(timeZone); dateFormat.setTimeZone(TimeZone.getTimeZone(timeZone)); } FilePath baseDir = logDir.newFilePath(Integer.toString(schema.getId())); if (schema.getPrimaryStorage().getBasePath() != null) baseDir = storageManager.resolveFilePath(schema.getPrimaryStorage().getBasePath()); String parentFileName = fileNamePrefix + dateFormat.format(day); List<FilePath> dataPathList = new ArrayList<FilePath>(); if (fileNameSuffix == null || fileNameSuffix.isEmpty()) { FilePath dataPath = new LocalFilePath(baseDir.getAbsolutePath() + "/" + parentFileName); if (dataPath.isFile()) dataPathList.add(dataPath); } else { String parentPathStr = baseDir.getAbsolutePath() + "/" + parentFileName.replace("\\", "/"); int lastBackslashIdx = parentPathStr.lastIndexOf("/"); final String remain = parentPathStr.substring(lastBackslashIdx + 1); FilePath parentPath = new LocalFilePath(parentPathStr.substring(0, lastBackslashIdx)); String[] suffixList = convertToRegex(fileNameSuffix).split("/"); dataPathList.addAll(retrieveFilesFromDate(parentPath.listFiles(new FilePathNameFilter() { @Override public boolean accept(FilePath dir, String name) { return name.startsWith(remain); } }), suffixList, 0, parentPathStr.length())); } String charset = retrieveConfig(primaryStorage, "charset", "utf-8"); try { return new LogFileReaderTxt(tableName, dataPathList, day, charset); } catch (Throwable t) { throw new IllegalStateException("cannot open reader txt: data file - " + dataPathList.get(0).getAbsolutePath()); } } private List<FilePath> retrieveFilesFromDate(FilePath[] files, String[] suffixList, int suffixListIdx, int recognizedCharCnt) { List<FilePath> matchedWithSuffix = new ArrayList<FilePath>(); if (files == null) return matchedWithSuffix; for (FilePath file : files) { String targetString = file.getAbsolutePath().substring(recognizedCharCnt); if (compareTargetAndSuffix(targetString, suffixList[suffixListIdx])) { if (suffixList.length == suffixListIdx + 1) { matchedWithSuffix.add(file); } else { matchedWithSuffix.addAll(retrieveFilesFromDate(file.listFiles(), suffixList, suffixListIdx + 1, recognizedCharCnt + targetString.length() + 1)); } } } return matchedWithSuffix; } @Override public List<TableConfigSpec> getConfigSpecs() { TableConfigSpec fileNamePrefix = newTableConfigSpec("filename_prefix", true, TableConfigSpec.locales("File name prefix", "파일명 접두사")); TableConfigSpec dateFormat = newTableConfigSpec("date_format", false, TableConfigSpec.locales("Date format", "날짜 형식")); TableConfigSpec fileNameSuffix = newTableConfigSpec("filename_suffix", true, TableConfigSpec.locales("File name suffix", "파일명 접미사")); TableConfigSpec dateLocale = newTableConfigSpec("date_locale", true, TableConfigSpec.locales("Date locale", "날짜 로케일")); TableConfigSpec timeZone = newTableConfigSpec("timezone", true, TableConfigSpec.locales("Time zone", "시간대")); TableConfigSpec charSet = newTableConfigSpec("charset", true, TableConfigSpec.locales("Charset", "문자집합")); return Arrays.asList(fileNamePrefix, dateFormat, fileNameSuffix, dateLocale, timeZone, charSet); } private TableConfigSpec newTableConfigSpec(String key, boolean optional, Map<Locale, String> displayNames) { TableConfigSpec newTableConfigSpec = new TableConfigSpec(); newTableConfigSpec.setKey(key); newTableConfigSpec.setOptional(optional); newTableConfigSpec.setDisplayNames(displayNames); return newTableConfigSpec; } @Override public Map<String, String> getConfigs() { return new HashMap<String, String>(); } @Override public void setConfig(String key, String value) { } @Override public void unsetConfig(String key) { } @Override public List<TableConfigSpec> getReplicaConfigSpecs() { return Arrays.asList(); } @Override public List<TableConfigSpec> getSecondaryConfigSpecs() { return Arrays.asList(); } }