{
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = 1L;
+
+ private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
+
+ public KeyExpiryMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
+ super(initialCapacity, loadFactor, concurrencyLevel);
+ }
+
+ public KeyExpiryMap(int initialCapacity, float loadFactor) {
+ super(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL);
+ }
+
+ public KeyExpiryMap(int initialCapacity) {
+ super(initialCapacity);
+ }
+
+ public KeyExpiryMap() {
+ super();
+ }
+
+ @Override
+ public synchronized Long get(Object key) {
+ if (this.containsKey(key)) {
+ return super.get(key);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public synchronized Long put(K key, Long expiryTimestamp) {
+ if (this.containsKey(key)) {
+ this.remove(key);
+ }
+ return super.put(key, expiryTimestamp);
+ }
+
+ @Override
+ public synchronized boolean containsKey(Object key) {
+ boolean result = false;
+ Long expiryTimestamp = super.get(key);
+ if (expiryTimestamp != null && System.currentTimeMillis() < expiryTimestamp) {
+ result = true;
+ } else {
+ this.remove(key);
+ }
+ return result;
+ }
+
+ @Override
+ public synchronized Long remove(Object key) {
+ return super.remove(key);
+ }
+
+ @Override
+ public synchronized void clear() {
+ super.clear();
+ }
+}
diff --git a/library/src/com/lidroid/xutils/cache/LruDiskCache.java b/library/src/com/lidroid/xutils/cache/LruDiskCache.java
new file mode 100644
index 0000000..6284c05
--- /dev/null
+++ b/library/src/com/lidroid/xutils/cache/LruDiskCache.java
@@ -0,0 +1,1224 @@
+/*
+ * Copyright (c) 2013. wyouflf (wyouflf@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.lidroid.xutils.cache;
+
+import com.lidroid.xutils.util.IOUtils;
+import com.lidroid.xutils.util.LogUtils;
+import org.apache.http.protocol.HTTP;
+
+import java.io.*;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A cache that uses a bounded amount of space on a filesystem. Each cache
+ * entry has a string key and a fixed number of values. Values are byte sequences,
+ * accessible as streams or files. Each value must be between {@code 0} and
+ * {@code Integer.MAX_VALUE} bytes in length.
+ *
+ * The cache stores its data in a directory on the filesystem. This
+ * directory must be exclusive to the cache; the cache may delete or overwrite
+ * files from its directory. It is an error for multiple processes to use the
+ * same cache directory at the same time.
+ *
+ * This cache limits the number of bytes that it will store on the
+ * filesystem. When the number of stored bytes exceeds the limit, the cache will
+ * remove entries in the background until the limit is satisfied. The limit is
+ * not strict: the cache may temporarily exceed it while waiting for files to be
+ * deleted. The limit does not include filesystem overhead or the cache
+ * journal so space-sensitive applications should set a conservative limit.
+ *
+ * Clients call {@link #edit} to create or update the values of an entry. An
+ * entry may have only one editor at one time; if a value is not available to be
+ * edited then {@link #edit} will return null.
+ *
+ * - When an entry is being created it is necessary to
+ * supply a full set of values; the empty value should be used as a
+ * placeholder if necessary.
+ *
- When an entry is being edited, it is not necessary
+ * to supply data for every value; values default to their previous
+ * value.
+ *
+ * Every {@link #edit} call must be matched by a call to {@link Editor#commit}
+ * or {@link Editor#abort}. Committing is atomic: a read observes the full set
+ * of values as they were before or after the commit, but never a mix of values.
+ *
+ * Clients call {@link #get} to read a snapshot of an entry. The read will
+ * observe the value at the time that {@link #get} was called. Updates and
+ * removals after the call do not impact ongoing reads.
+ *
+ * This class is tolerant of some I/O errors. If files are missing from the
+ * filesystem, the corresponding entries will be dropped from the cache. If
+ * an error occurs while writing a cache value, the edit will fail silently.
+ * Callers should handle other problems by catching {@code IOException} and
+ * responding appropriately.
+ */
+public final class LruDiskCache implements Closeable {
+ static final String JOURNAL_FILE = "journal";
+ static final String JOURNAL_FILE_TEMP = "journal.tmp";
+ static final String JOURNAL_FILE_BACKUP = "journal.bkp";
+ static final String MAGIC = "libcore.io.DiskLruCache";
+ static final String VERSION = "1";
+ static final long ANY_SEQUENCE_NUMBER = -1;
+ private static final char CLEAN = 'C';
+ private static final char UPDATE = 'U';
+ private static final char DELETE = 'D';
+ private static final char READ = 'R';
+ private static final char EXPIRY_PREFIX = 't';
+
+ /*
+ * This cache uses a journal file named "journal". A typical journal file
+ * looks like this:
+ * libcore.io.DiskLruCache
+ * 1
+ * 100
+ * 2
+ *
+ * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
+ * UPDATE 335c4c6028171cfddfbaae1a9c313c52
+ * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
+ * DELETE 335c4c6028171cfddfbaae1a9c313c52
+ * UPDATE 1ab96a171faeeee38496d8b330771a7a
+ * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
+ * READ 335c4c6028171cfddfbaae1a9c313c52
+ * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
+ *
+ * The first five lines of the journal form its header. They are the
+ * constant string "libcore.io.DiskLruCache", the disk cache's version,
+ * the application's version, the value count, and a blank line.
+ *
+ * Each of the subsequent lines in the file is a record of the state of a
+ * cache entry. Each line contains space-separated values: a state, a key,
+ * and optional state-specific values.
+ * o UPDATE lines track that an entry is actively being created or updated.
+ * Every successful UPDATE action should be followed by a CLEAN or DELETE
+ * action. UPDATE lines without a matching CLEAN or DELETE indicate that
+ * temporary files may need to be deleted.
+ * o CLEAN lines track a cache entry that has been successfully published
+ * and may be read. A publish line is followed by the lengths of each of
+ * its values.
+ * o READ lines track accesses for LRU.
+ * o DELETE lines track entries that have been deleted.
+ *
+ * The journal file is appended to as cache operations occur. The journal may
+ * occasionally be compacted by dropping redundant lines. A temporary file named
+ * "journal.tmp" will be used during compaction; that file should be deleted if
+ * it exists when the cache is opened.
+ */
+
+ private final File directory;
+ private final File journalFile;
+ private final File journalFileTmp;
+ private final File journalFileBackup;
+ private final int appVersion;
+ private long maxSize;
+ private final int valueCount;
+ private long size = 0;
+ private Writer journalWriter;
+ private final LinkedHashMap lruEntries =
+ new LinkedHashMap(0, 0.75f, true);
+ private int redundantOpCount;
+
+ /**
+ * To differentiate between old and current snapshots, each entry is given
+ * a sequence number each time an edit is committed. A snapshot is stale if
+ * its sequence number is not equal to its entry's sequence number.
+ */
+ private long nextSequenceNumber = 0;
+
+ /**
+ * This cache uses a single background thread to evict entries.
+ */
+ final ThreadPoolExecutor executorService =
+ new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue());
+ private final Callable cleanupCallable = new Callable() {
+ public Void call() throws Exception {
+ synchronized (LruDiskCache.this) {
+ if (journalWriter == null) {
+ return null; // Closed.
+ }
+ trimToSize();
+ if (journalRebuildRequired()) {
+ rebuildJournal();
+ redundantOpCount = 0;
+ }
+ }
+ return null;
+ }
+ };
+
+ private LruDiskCache(File directory, int appVersion, int valueCount, long maxSize) {
+ this.directory = directory;
+ this.appVersion = appVersion;
+ this.journalFile = new File(directory, JOURNAL_FILE);
+ this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
+ this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
+ this.valueCount = valueCount;
+ this.maxSize = maxSize;
+ }
+
+ /**
+ * Opens the cache in {@code directory}, creating a cache if none exists
+ * there.
+ *
+ * @param directory a writable directory
+ * @param valueCount the number of values per cache entry. Must be positive.
+ * @param maxSize the maximum number of bytes this cache should use to store
+ * @throws IOException if reading or writing the cache directory fails
+ */
+ public static LruDiskCache open(File directory, int appVersion, int valueCount, long maxSize)
+ throws IOException {
+ if (maxSize <= 0) {
+ throw new IllegalArgumentException("maxSize <= 0");
+ }
+ if (valueCount <= 0) {
+ throw new IllegalArgumentException("valueCount <= 0");
+ }
+
+ // If a bkp file exists, use it instead.
+ File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
+ if (backupFile.exists()) {
+ File journalFile = new File(directory, JOURNAL_FILE);
+ // If journal file also exists just delete backup file.
+ if (journalFile.exists()) {
+ backupFile.delete();
+ } else {
+ renameTo(backupFile, journalFile, false);
+ }
+ }
+
+ // Prefer to pick up where we left off.
+ LruDiskCache cache = new LruDiskCache(directory, appVersion, valueCount, maxSize);
+ if (cache.journalFile.exists()) {
+ try {
+ cache.readJournal();
+ cache.processJournal();
+ cache.journalWriter = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), HTTP.US_ASCII));
+ return cache;
+ } catch (Throwable journalIsCorrupt) {
+ LogUtils.e("DiskLruCache "
+ + directory
+ + " is corrupt: "
+ + journalIsCorrupt.getMessage()
+ + ", removing", journalIsCorrupt);
+ cache.delete();
+ }
+ }
+
+ // Create a new empty cache.
+ if (directory.exists() || directory.mkdirs()) {
+ cache = new LruDiskCache(directory, appVersion, valueCount, maxSize);
+ cache.rebuildJournal();
+ }
+ return cache;
+ }
+
+ private void readJournal() throws IOException {
+ StrictLineReader reader = null;
+ try {
+ reader = new StrictLineReader(new FileInputStream(journalFile));
+ String magic = reader.readLine();
+ String version = reader.readLine();
+ String appVersionString = reader.readLine();
+ String valueCountString = reader.readLine();
+ String blank = reader.readLine();
+ if (!MAGIC.equals(magic)
+ || !VERSION.equals(version)
+ || !Integer.toString(appVersion).equals(appVersionString)
+ || !Integer.toString(valueCount).equals(valueCountString)
+ || !"".equals(blank)) {
+ throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
+ + valueCountString + ", " + blank + "]");
+ }
+
+ int lineCount = 0;
+ while (true) {
+ try {
+ readJournalLine(reader.readLine());
+ lineCount++;
+ } catch (EOFException endOfJournal) {
+ break;
+ }
+ }
+ redundantOpCount = lineCount - lruEntries.size();
+ } finally {
+ IOUtils.closeQuietly(reader);
+ }
+ }
+
+ private void readJournalLine(String line) throws IOException {
+ int firstSpace = line.indexOf(' ');
+ char lineTag = 0;
+ if (firstSpace == 1) {
+ lineTag = line.charAt(0);
+ } else {
+ throw new IOException("unexpected journal line: " + line);
+ }
+
+ int keyBegin = firstSpace + 1;
+ int secondSpace = line.indexOf(' ', keyBegin);
+ final String diskKey;
+ if (secondSpace == -1) {
+ diskKey = line.substring(keyBegin);
+ if (lineTag == DELETE) {
+ lruEntries.remove(diskKey);
+ return;
+ }
+ } else {
+ diskKey = line.substring(keyBegin, secondSpace);
+ }
+
+ Entry entry = lruEntries.get(diskKey);
+ if (entry == null) {
+ entry = new Entry(diskKey);
+ lruEntries.put(diskKey, entry);
+ }
+
+ switch (lineTag) {
+ case CLEAN: {
+ entry.readable = true;
+ entry.currentEditor = null;
+ String[] parts = line.substring(secondSpace + 1).split(" ");
+ if (parts.length > 0) {
+ try {
+ if (parts[0].charAt(0) == EXPIRY_PREFIX) {
+ entry.expiryTimestamp = Long.valueOf(parts[0].substring(1));
+ entry.setLengths(parts, 1);
+ } else {
+ entry.expiryTimestamp = Long.MAX_VALUE;
+ entry.setLengths(parts, 0);
+ }
+ } catch (Throwable e) {
+ throw new IOException("unexpected journal line: " + line);
+ }
+ }
+ break;
+ }
+ case UPDATE: {
+ entry.currentEditor = new Editor(entry);
+ break;
+ }
+ case READ: {
+ // This work was already done by calling lruEntries.get().
+ break;
+ }
+ default: {
+ throw new IOException("unexpected journal line: " + line);
+ }
+ }
+ }
+
+ /**
+ * Computes the initial size and collects garbage as a part of opening the
+ * cache. Dirty entries are assumed to be inconsistent and will be deleted.
+ */
+ private void processJournal() throws IOException {
+ deleteIfExists(journalFileTmp);
+ for (Iterator i = lruEntries.values().iterator(); i.hasNext(); ) {
+ Entry entry = i.next();
+ if (entry.currentEditor == null) {
+ for (int t = 0; t < valueCount; t++) {
+ size += entry.lengths[t];
+ }
+ } else {
+ entry.currentEditor = null;
+ for (int t = 0; t < valueCount; t++) {
+ deleteIfExists(entry.getCleanFile(t));
+ deleteIfExists(entry.getDirtyFile(t));
+ }
+ i.remove();
+ }
+ }
+ }
+
+ /**
+ * Creates a new journal that omits redundant information. This replaces the
+ * current journal if it exists.
+ */
+ private synchronized void rebuildJournal() throws IOException {
+ if (journalWriter != null) {
+ IOUtils.closeQuietly(journalWriter);
+ }
+
+ Writer writer = null;
+ try {
+ writer = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(journalFileTmp), HTTP.US_ASCII));
+ writer.write(MAGIC);
+ writer.write("\n");
+ writer.write(VERSION);
+ writer.write("\n");
+ writer.write(Integer.toString(appVersion));
+ writer.write("\n");
+ writer.write(Integer.toString(valueCount));
+ writer.write("\n");
+ writer.write("\n");
+
+ for (Entry entry : lruEntries.values()) {
+ if (entry.currentEditor != null) {
+ writer.write(UPDATE + " " + entry.diskKey + '\n');
+ } else {
+ writer.write(CLEAN + " " + entry.diskKey + " " + EXPIRY_PREFIX + entry.expiryTimestamp + entry.getLengths() + '\n');
+ }
+ }
+ } finally {
+ IOUtils.closeQuietly(writer);
+ }
+
+ if (journalFile.exists()) {
+ renameTo(journalFile, journalFileBackup, true);
+ }
+ renameTo(journalFileTmp, journalFile, false);
+ journalFileBackup.delete();
+
+ journalWriter = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(journalFile, true), HTTP.US_ASCII));
+ }
+
+ private static void deleteIfExists(File file) throws IOException {
+ if (file.exists() && !file.delete()) {
+ throw new IOException();
+ }
+ }
+
+ private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
+ if (deleteDestination) {
+ deleteIfExists(to);
+ }
+ if (!from.renameTo(to)) {
+ throw new IOException();
+ }
+ }
+
+ public synchronized long getExpiryTimestamp(String key) throws IOException {
+ String diskKey = fileNameGenerator.generate(key);
+ checkNotClosed();
+ Entry entry = lruEntries.get(diskKey);
+ if (entry == null) {
+ return 0;
+ } else {
+ return entry.expiryTimestamp;
+ }
+ }
+
+ public File getCacheFile(String key, int index) {
+ String diskKey = fileNameGenerator.generate(key);
+ File result = new File(this.directory, diskKey + "." + index);
+ if (result.exists()) {
+ return result;
+ } else {
+ try {
+ this.remove(key);
+ } catch (IOException ignore) {
+ }
+ return null;
+ }
+ }
+
+ public Snapshot get(String key) throws IOException {
+ String diskKey = fileNameGenerator.generate(key);
+ return getByDiskKey(diskKey);
+ }
+
+ /**
+ * Returns a snapshot of the entry named {@code diskKey}, or null if it doesn't
+ * exist is not currently readable. If a value is returned, it is moved to
+ * the head of the LRU queue.
+ */
+ private synchronized Snapshot getByDiskKey(String diskKey) throws IOException {
+ checkNotClosed();
+ Entry entry = lruEntries.get(diskKey);
+ if (entry == null) {
+ return null;
+ }
+
+ if (!entry.readable) {
+ return null;
+ }
+
+ // If expired, delete the entry.
+ if (entry.expiryTimestamp < System.currentTimeMillis()) {
+ for (int i = 0; i < valueCount; i++) {
+ File file = entry.getCleanFile(i);
+ if (file.exists() && !file.delete()) {
+ throw new IOException("failed to delete " + file);
+ }
+ size -= entry.lengths[i];
+ entry.lengths[i] = 0;
+ }
+ redundantOpCount++;
+ journalWriter.append(DELETE + " " + diskKey + '\n');
+ lruEntries.remove(diskKey);
+ if (journalRebuildRequired()) {
+ executorService.submit(cleanupCallable);
+ }
+ return null;
+ }
+
+ // Open all streams eagerly to guarantee that we see a single published
+ // snapshot. If we opened streams lazily then the streams could come
+ // from different edits.
+ FileInputStream[] ins = new FileInputStream[valueCount];
+ try {
+ for (int i = 0; i < valueCount; i++) {
+ ins[i] = new FileInputStream(entry.getCleanFile(i));
+ }
+ } catch (FileNotFoundException e) {
+ // A file must have been deleted manually!
+ for (int i = 0; i < valueCount; i++) {
+ if (ins[i] != null) {
+ IOUtils.closeQuietly(ins[i]);
+ } else {
+ break;
+ }
+ }
+ return null;
+ }
+
+ redundantOpCount++;
+ journalWriter.append(READ + " " + diskKey + '\n');
+ if (journalRebuildRequired()) {
+ executorService.submit(cleanupCallable);
+ }
+
+ return new Snapshot(diskKey, entry.sequenceNumber, ins, entry.lengths);
+ }
+
+ /**
+ * Returns an editor for the entry named {@code Key}, or null if another
+ * edit is in progress.
+ */
+ public Editor edit(String key) throws IOException {
+ String diskKey = fileNameGenerator.generate(key);
+ return editByDiskKey(diskKey, ANY_SEQUENCE_NUMBER);
+ }
+
+ private synchronized Editor editByDiskKey(String diskKey, long expectedSequenceNumber) throws IOException {
+ checkNotClosed();
+ Entry entry = lruEntries.get(diskKey);
+ if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER &&
+ (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
+ return null; // Snapshot is stale.
+ }
+ if (entry == null) {
+ entry = new Entry(diskKey);
+ lruEntries.put(diskKey, entry);
+ } else if (entry.currentEditor != null) {
+ return null; // Another edit is in progress.
+ }
+
+ Editor editor = new Editor(entry);
+ entry.currentEditor = editor;
+
+ // Flush the journal before creating files to prevent file leaks.
+ journalWriter.write(UPDATE + " " + diskKey + '\n');
+ journalWriter.flush();
+ return editor;
+ }
+
+ /**
+ * Returns the directory where this cache stores its data.
+ */
+ public File getDirectory() {
+ return directory;
+ }
+
+ /**
+ * Returns the maximum number of bytes that this cache should use to store
+ * its data.
+ */
+ public synchronized long getMaxSize() {
+ return maxSize;
+ }
+
+ /**
+ * Changes the maximum number of bytes the cache can store and queues a job
+ * to trim the existing store, if necessary.
+ */
+ public synchronized void setMaxSize(long maxSize) {
+ this.maxSize = maxSize;
+ executorService.submit(cleanupCallable);
+ }
+
+ /**
+ * Returns the number of bytes currently being used to store the values in
+ * this cache. This may be greater than the max size if a background
+ * deletion is pending.
+ */
+ public synchronized long size() {
+ return size;
+ }
+
+ private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
+ Entry entry = editor.entry;
+ if (entry.currentEditor != editor) {
+ throw new IllegalStateException();
+ }
+
+ // If this edit is creating the entry for the first time, every index must have a value.
+ if (success && !entry.readable) {
+ for (int i = 0; i < valueCount; i++) {
+ if (!editor.written[i]) {
+ editor.abort();
+ throw new IllegalStateException("Newly created entry didn't create value for index " + i);
+ }
+ if (!entry.getDirtyFile(i).exists()) {
+ editor.abort();
+ return;
+ }
+ }
+ }
+
+ for (int i = 0; i < valueCount; i++) {
+ File dirty = entry.getDirtyFile(i);
+ if (success) {
+ if (dirty.exists()) {
+ File clean = entry.getCleanFile(i);
+ dirty.renameTo(clean);
+ long oldLength = entry.lengths[i];
+ long newLength = clean.length();
+ entry.lengths[i] = newLength;
+ size = size - oldLength + newLength;
+ }
+ } else {
+ deleteIfExists(dirty);
+ }
+ }
+
+ redundantOpCount++;
+ entry.currentEditor = null;
+ if (entry.readable | success) {
+ entry.readable = true;
+ journalWriter.write(CLEAN + " " + entry.diskKey + " " + EXPIRY_PREFIX + entry.expiryTimestamp + entry.getLengths() + '\n');
+ if (success) {
+ entry.sequenceNumber = nextSequenceNumber++;
+ }
+ } else {
+ lruEntries.remove(entry.diskKey);
+ journalWriter.write(DELETE + " " + entry.diskKey + '\n');
+ }
+ journalWriter.flush();
+
+ if (size > maxSize || journalRebuildRequired()) {
+ executorService.submit(cleanupCallable);
+ }
+ }
+
+ /**
+ * We only rebuild the journal when it will halve the size of the journal
+ * and eliminate at least 2000 ops.
+ */
+ private boolean journalRebuildRequired() {
+ final int redundantOpCompactThreshold = 2000;
+ return redundantOpCount >= redundantOpCompactThreshold //
+ && redundantOpCount >= lruEntries.size();
+ }
+
+ public boolean remove(String key) throws IOException {
+ String diskKey = fileNameGenerator.generate(key);
+ return removeByDiskKey(diskKey);
+ }
+
+ /**
+ * Drops the entry for {@code diskKey} if it exists and can be removed. Entries
+ * actively being edited cannot be removed.
+ *
+ * @return true if an entry was removed.
+ */
+ private synchronized boolean removeByDiskKey(String diskKey) throws IOException {
+ checkNotClosed();
+ Entry entry = lruEntries.get(diskKey);
+ if (entry == null || entry.currentEditor != null) {
+ return false;
+ }
+
+ for (int i = 0; i < valueCount; i++) {
+ File file = entry.getCleanFile(i);
+ if (file.exists() && !file.delete()) {
+ throw new IOException("failed to delete " + file);
+ }
+ size -= entry.lengths[i];
+ entry.lengths[i] = 0;
+ }
+
+ redundantOpCount++;
+ journalWriter.append(DELETE + " " + diskKey + '\n');
+ lruEntries.remove(diskKey);
+
+ if (journalRebuildRequired()) {
+ executorService.submit(cleanupCallable);
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns true if this cache has been closed.
+ */
+ public synchronized boolean isClosed() {
+ return journalWriter == null;
+ }
+
+ private void checkNotClosed() {
+ if (journalWriter == null) {
+ throw new IllegalStateException("cache is closed");
+ }
+ }
+
+ /**
+ * Force buffered operations to the filesystem.
+ */
+ public synchronized void flush() throws IOException {
+ checkNotClosed();
+ trimToSize();
+ journalWriter.flush();
+ }
+
+ /**
+ * Closes this cache. Stored values will remain on the filesystem.
+ */
+ @Override
+ public synchronized void close() throws IOException {
+ if (journalWriter == null) {
+ return; // Already closed.
+ }
+ for (Entry entry : new ArrayList(lruEntries.values())) {
+ if (entry.currentEditor != null) {
+ entry.currentEditor.abort();
+ }
+ }
+ trimToSize();
+ journalWriter.close();
+ journalWriter = null;
+ }
+
+ private void trimToSize() throws IOException {
+ while (size > maxSize) {
+ Map.Entry toEvict = lruEntries.entrySet().iterator().next();
+ removeByDiskKey(toEvict.getKey());
+ }
+ }
+
+ /**
+ * Closes the cache and deletes all of its stored values. This will delete
+ * all files in the cache directory including files that weren't created by
+ * the cache.
+ */
+ public void delete() throws IOException {
+ IOUtils.closeQuietly(this);
+ deleteContents(directory);
+ }
+
+ private static String inputStreamToString(InputStream in) throws IOException {
+ return readFully(new InputStreamReader(in, HTTP.UTF_8));
+ }
+
+ /**
+ * A snapshot of the values for an entry.
+ */
+ public final class Snapshot implements Closeable {
+ private final String diskKey;
+ private final long sequenceNumber;
+ private final FileInputStream[] ins;
+ private final long[] lengths;
+
+ private Snapshot(String diskKey, long sequenceNumber, FileInputStream[] ins, long[] lengths) {
+ this.diskKey = diskKey;
+ this.sequenceNumber = sequenceNumber;
+ this.ins = ins;
+ this.lengths = lengths;
+ }
+
+ /**
+ * Returns an editor for this snapshot's entry, or null if either the
+ * entry has changed since this snapshot was created or if another edit
+ * is in progress.
+ */
+ public Editor edit() throws IOException {
+ return LruDiskCache.this.editByDiskKey(diskKey, sequenceNumber);
+ }
+
+ /**
+ * Returns the unbuffered stream with the value for {@code index}.
+ */
+ public FileInputStream getInputStream(int index) {
+ return ins[index];
+ }
+
+ /**
+ * Returns the string value for {@code index}.
+ */
+ public String getString(int index) throws IOException {
+ return inputStreamToString(getInputStream(index));
+ }
+
+ /**
+ * Returns the byte length of the value for {@code index}.
+ */
+ public long getLength(int index) {
+ return lengths[index];
+ }
+
+ @Override
+ public void close() {
+ for (InputStream in : ins) {
+ IOUtils.closeQuietly(in);
+ }
+ }
+ }
+
+ private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {
+ @Override
+ public void write(int b) throws IOException {
+ // Eat all writes silently. Nom nom.
+ }
+ };
+
+ /**
+ * Edits the values for an entry.
+ */
+ public final class Editor {
+ private final Entry entry;
+ private final boolean[] written;
+ private boolean hasErrors;
+ private boolean committed;
+
+ private Editor(Entry entry) {
+ this.entry = entry;
+ this.written = (entry.readable) ? null : new boolean[valueCount];
+ }
+
+ public void setEntryExpiryTimestamp(long timestamp) {
+ entry.expiryTimestamp = timestamp;
+ }
+
+ /**
+ * Returns an unbuffered input stream to read the last committed value,
+ * or null if no value has been committed.
+ */
+ public InputStream newInputStream(int index) throws IOException {
+ synchronized (LruDiskCache.this) {
+ if (entry.currentEditor != this) {
+ throw new IllegalStateException();
+ }
+ if (!entry.readable) {
+ return null;
+ }
+ try {
+ return new FileInputStream(entry.getCleanFile(index));
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Returns the last committed value as a string, or null if no value
+ * has been committed.
+ */
+ public String getString(int index) throws IOException {
+ InputStream in = newInputStream(index);
+ return in != null ? inputStreamToString(in) : null;
+ }
+
+ /**
+ * Returns a new unbuffered output stream to write the value at
+ * {@code index}. If the underlying output stream encounters errors
+ * when writing to the filesystem, this edit will be aborted when
+ * {@link #commit} is called. The returned output stream does not throw
+ * IOExceptions.
+ */
+ public OutputStream newOutputStream(int index) throws IOException {
+ synchronized (LruDiskCache.this) {
+ if (entry.currentEditor != this) {
+ throw new IllegalStateException();
+ }
+ if (!entry.readable) {
+ written[index] = true;
+ }
+ File dirtyFile = entry.getDirtyFile(index);
+ FileOutputStream outputStream;
+ try {
+ outputStream = new FileOutputStream(dirtyFile);
+ } catch (FileNotFoundException e) {
+ // Attempt to recreate the cache directory.
+ directory.mkdirs();
+ try {
+ outputStream = new FileOutputStream(dirtyFile);
+ } catch (FileNotFoundException e2) {
+ // We are unable to recover. Silently eat the writes.
+ return NULL_OUTPUT_STREAM;
+ }
+ }
+ return new FaultHidingOutputStream(outputStream);
+ }
+ }
+
+ /**
+ * Sets the value at {@code index} to {@code value}.
+ */
+ public void set(int index, String value) throws IOException {
+ Writer writer = null;
+ try {
+ writer = new OutputStreamWriter(newOutputStream(index), HTTP.UTF_8);
+ writer.write(value);
+ } finally {
+ IOUtils.closeQuietly(writer);
+ }
+ }
+
+ /**
+ * Commits this edit so it is visible to readers. This releases the
+ * edit lock so another edit may be started on the same key.
+ */
+ public void commit() throws IOException {
+ if (hasErrors) {
+ completeEdit(this, false);
+ removeByDiskKey(entry.diskKey); // The previous entry is stale.
+ } else {
+ completeEdit(this, true);
+ }
+ committed = true;
+ }
+
+ /**
+ * Aborts this edit. This releases the edit lock so another edit may be
+ * started on the same key.
+ */
+ public void abort() throws IOException {
+ completeEdit(this, false);
+ }
+
+ public void abortUnlessCommitted() {
+ if (!committed) {
+ try {
+ abort();
+ } catch (Throwable ignored) {
+ }
+ }
+ }
+
+ private class FaultHidingOutputStream extends FilterOutputStream {
+ private FaultHidingOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ @Override
+ public void write(int oneByte) {
+ try {
+ out.write(oneByte);
+ } catch (Throwable e) {
+ hasErrors = true;
+ }
+ }
+
+ @Override
+ public void write(byte[] buffer, int offset, int length) {
+ try {
+ out.write(buffer, offset, length);
+ out.flush();
+ } catch (Throwable e) {
+ hasErrors = true;
+ }
+ }
+
+ @Override
+ public void close() {
+ try {
+ out.close();
+ } catch (Throwable e) {
+ hasErrors = true;
+ }
+ }
+
+ @Override
+ public void flush() {
+ try {
+ out.flush();
+ } catch (Throwable e) {
+ hasErrors = true;
+ }
+ }
+ }
+ }
+
+ private final class Entry {
+ private final String diskKey;
+
+ private long expiryTimestamp = Long.MAX_VALUE;
+
+ /**
+ * Lengths of this entry's files.
+ */
+ private final long[] lengths;
+
+ /**
+ * True if this entry has ever been published.
+ */
+ private boolean readable;
+
+ /**
+ * The ongoing edit or null if this entry is not being edited.
+ */
+ private Editor currentEditor;
+
+ /**
+ * The sequence number of the most recently committed edit to this entry.
+ */
+ private long sequenceNumber;
+
+ private Entry(String diskKey) {
+ this.diskKey = diskKey;
+ this.lengths = new long[valueCount];
+ }
+
+ public String getLengths() throws IOException {
+ StringBuilder result = new StringBuilder();
+ for (long size : lengths) {
+ result.append(" ").append(size);
+ }
+ return result.toString();
+ }
+
+ /**
+ * Set lengths using decimal numbers like "10123".
+ */
+ private void setLengths(String[] strings, int startIndex) throws IOException {
+ if ((strings.length - startIndex) != valueCount) {
+ throw invalidLengths(strings);
+ }
+
+ try {
+ for (int i = 0; i < valueCount; i++) {
+ lengths[i] = Long.parseLong(strings[i + startIndex]);
+ }
+ } catch (NumberFormatException e) {
+ throw invalidLengths(strings);
+ }
+ }
+
+ private IOException invalidLengths(String[] strings) throws IOException {
+ throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
+ }
+
+ public File getCleanFile(int i) {
+ return new File(directory, diskKey + "." + i);
+ }
+
+ public File getDirtyFile(int i) {
+ return new File(directory, diskKey + "." + i + ".tmp");
+ }
+ }
+
+ /////////////////////////////////////// utils //////////////////////////////////////////////////////////////////////
+ private static String readFully(Reader reader) throws IOException {
+ StringWriter writer = null;
+ try {
+ writer = new StringWriter();
+ char[] buffer = new char[1024];
+ int count;
+ while ((count = reader.read(buffer)) != -1) {
+ writer.write(buffer, 0, count);
+ }
+ return writer.toString();
+ } finally {
+ IOUtils.closeQuietly(reader);
+ IOUtils.closeQuietly(writer);
+ }
+ }
+
+ /**
+ * Deletes the contents of {@code dir}. Throws an IOException if any file
+ * could not be deleted, or if {@code dir} is not a readable directory.
+ */
+ private static void deleteContents(File dir) throws IOException {
+ File[] files = dir.listFiles();
+ if (files == null) {
+ throw new IOException("not a readable directory: " + dir);
+ }
+ for (File file : files) {
+ if (file.isDirectory()) {
+ deleteContents(file);
+ }
+ if (file.exists() && !file.delete()) {
+ throw new IOException("failed to delete file: " + file);
+ }
+ }
+ }
+
+ /////////////////////////////////////// StrictLineReader /////////////////////////////////////////////
+ private class StrictLineReader implements Closeable {
+ private static final byte CR = (byte) '\r';
+ private static final byte LF = (byte) '\n';
+
+ private final InputStream in;
+ private final Charset charset = Charset.forName(HTTP.US_ASCII);
+
+ /*
+ * Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
+ * and the data in the range [pos, end) is buffered for reading. At end of input, if there is
+ * an unterminated line, we set end == -1, otherwise end == pos. If the underlying
+ * {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
+ */
+ private byte[] buf;
+ private int pos;
+ private int end;
+
+ /**
+ * Constructs a new {@code LineReader} with the specified charset and the default capacity.
+ *
+ * @param in the {@code InputStream} to read data from.
+ * @throws NullPointerException if {@code in} or {@code charset} is null.
+ * @throws IllegalArgumentException if the specified charset is not supported.
+ */
+ public StrictLineReader(InputStream in) {
+ this(in, 8192);
+ }
+
+ /**
+ * Constructs a new {@code LineReader} with the specified capacity and charset.
+ *
+ * @param in the {@code InputStream} to read data from.
+ * @param capacity the capacity of the buffer.
+ * @throws NullPointerException if {@code in} or {@code charset} is null.
+ * @throws IllegalArgumentException if {@code capacity} is negative or zero
+ * or the specified charset is not supported.
+ */
+ public StrictLineReader(InputStream in, int capacity) {
+ if (in == null) {
+ throw new NullPointerException();
+ }
+ if (capacity < 0) {
+ throw new IllegalArgumentException("capacity <= 0");
+ }
+
+ this.in = in;
+ buf = new byte[capacity];
+ }
+
+ /**
+ * Closes the reader by closing the underlying {@code InputStream} and
+ * marking this reader as closed.
+ *
+ * @throws IOException for errors when closing the underlying {@code InputStream}.
+ */
+ @Override
+ public void close() throws IOException {
+ synchronized (in) {
+ if (buf != null) {
+ buf = null;
+ in.close();
+ }
+ }
+ }
+
+ /**
+ * Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"},
+ * this end of line marker is not included in the result.
+ *
+ * @return the next line from the input.
+ * @throws IOException for underlying {@code InputStream} errors.
+ * @throws EOFException for the end of source stream.
+ */
+ public String readLine() throws IOException {
+ synchronized (in) {
+ if (buf == null) {
+ throw new IOException("LineReader is closed");
+ }
+
+ // Read more data if we are at the end of the buffered data.
+ // Though it's an error to read after an exception, we will let {@code fillBuf()}
+ // throw again if that happens; thus we need to handle end == -1 as well as end == pos.
+ if (pos >= end) {
+ fillBuf();
+ }
+ // Try to find LF in the buffered data and return the line if successful.
+ for (int i = pos; i != end; ++i) {
+ if (buf[i] == LF) {
+ int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
+ String res = new String(buf, pos, lineEnd - pos, charset.name());
+ pos = i + 1;
+ return res;
+ }
+ }
+
+ // Let's anticipate up to 80 characters on top of those already read.
+ ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
+ @Override
+ public String toString() {
+ int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
+ try {
+ return new String(buf, 0, length, charset.name());
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e); // Since we control the charset this will never happen.
+ }
+ }
+ };
+
+ while (true) {
+ out.write(buf, pos, end - pos);
+ // Mark unterminated line in case fillBuf throws EOFException or IOException.
+ end = -1;
+ fillBuf();
+ // Try to find LF in the buffered data and return the line if successful.
+ for (int i = pos; i != end; ++i) {
+ if (buf[i] == LF) {
+ if (i != pos) {
+ out.write(buf, pos, i - pos);
+ }
+ out.flush();
+ pos = i + 1;
+ return out.toString();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Reads new input data into the buffer. Call only with pos == end or end == -1,
+ * depending on the desired outcome if the function throws.
+ */
+ private void fillBuf() throws IOException {
+ int result = in.read(buf, 0, buf.length);
+ if (result == -1) {
+ throw new EOFException();
+ }
+ pos = 0;
+ end = result;
+ }
+ }
+
+ private FileNameGenerator fileNameGenerator = new MD5FileNameGenerator();
+
+ public FileNameGenerator getFileNameGenerator() {
+ return fileNameGenerator;
+ }
+
+ public void setFileNameGenerator(FileNameGenerator fileNameGenerator) {
+ if (fileNameGenerator != null) {
+ this.fileNameGenerator = fileNameGenerator;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/lidroid/xutils/bitmap/core/LruMemoryCache.java b/library/src/com/lidroid/xutils/cache/LruMemoryCache.java
similarity index 88%
rename from src/com/lidroid/xutils/bitmap/core/LruMemoryCache.java
rename to library/src/com/lidroid/xutils/cache/LruMemoryCache.java
index e62801a..191d238 100644
--- a/src/com/lidroid/xutils/bitmap/core/LruMemoryCache.java
+++ b/library/src/com/lidroid/xutils/cache/LruMemoryCache.java
@@ -13,7 +13,7 @@
* limitations under the License.
*/
-package com.lidroid.xutils.bitmap.core;
+package com.lidroid.xutils.cache;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -33,6 +33,12 @@ public class LruMemoryCache {
private int hitCount;
private int missCount;
+ /**
+ * key: K
+ * value: expiry time
+ */
+ private KeyExpiryMap keyExpiryMap;
+
/**
* @param maxSize for caches that do not override {@link #sizeOf}, this is
* the maximum number of entries in the cache. For all other caches,
@@ -44,6 +50,12 @@ public LruMemoryCache(int maxSize) {
}
this.maxSize = maxSize;
this.map = new LinkedHashMap(0, 0.75f, true);
+ this.keyExpiryMap = new KeyExpiryMap(0, 0.75f);
+ }
+
+ public void setMaxSize(int maxSize) {
+ this.maxSize = maxSize;
+ trimToSize(maxSize);
}
/**
@@ -59,6 +71,11 @@ public final V get(K key) {
V mapValue;
synchronized (this) {
+ // If expired, remove the entry.
+ if (!keyExpiryMap.containsKey(key)) {
+ this.remove(key);
+ return null;
+ }
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
@@ -103,10 +120,21 @@ public final V get(K key) {
/**
* Caches {@code value} for {@code key}. The value is moved to the head of
* the queue.
+ * Default expiryTimestamp: Long.MAX_VALUE.
*
* @return the previous value mapped by {@code key}.
*/
public final V put(K key, V value) {
+ return put(key, value, Long.MAX_VALUE);
+ }
+
+ /**
+ * Caches {@code value} for {@code key}. The value is moved to the head of
+ * the queue.
+ *
+ * @return the previous value mapped by {@code key}.
+ */
+ public final V put(K key, V value, long expiryTimestamp) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
@@ -116,6 +144,7 @@ public final V put(K key, V value) {
putCount++;
size += safeSizeOf(key, value);
previous = map.put(key, value);
+ keyExpiryMap.put(key, expiryTimestamp);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
@@ -138,11 +167,6 @@ private void trimToSize(int maxSize) {
K key;
V value;
synchronized (this) {
- if (size < 0 || (map.isEmpty() && size != 0)) {
- throw new IllegalStateException(getClass().getName()
- + ".sizeOf() is reporting inconsistent results!");
- }
-
if (size <= maxSize || map.isEmpty()) {
break;
}
@@ -151,6 +175,7 @@ private void trimToSize(int maxSize) {
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
+ keyExpiryMap.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
@@ -172,6 +197,7 @@ public final V remove(K key) {
V previous;
synchronized (this) {
previous = map.remove(key);
+ keyExpiryMap.remove(key);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
@@ -184,6 +210,10 @@ public final V remove(K key) {
return previous;
}
+ public final boolean containsKey(K key) {
+ return map.containsKey(key);
+ }
+
/**
* Called for entries that have been evicted or removed. This method is
* invoked when a value is evicted to make space, removed by a call to
@@ -223,8 +253,11 @@ protected V create(K key) {
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
- if (result < 0) {
- throw new IllegalStateException("Negative size: " + key + "=" + value);
+ if (result <= 0) {
+ size = 0;
+ for (Map.Entry entry : map.entrySet()) {
+ size += sizeOf(entry.getKey(), entry.getValue());
+ }
}
return result;
}
@@ -245,6 +278,7 @@ protected int sizeOf(K key, V value) {
*/
public final void evictAll() {
trimToSize(-1); // -1 will evict 0-sized elements
+ keyExpiryMap.clear();
}
/**
diff --git a/library/src/com/lidroid/xutils/cache/MD5FileNameGenerator.java b/library/src/com/lidroid/xutils/cache/MD5FileNameGenerator.java
new file mode 100644
index 0000000..3dbe8c4
--- /dev/null
+++ b/library/src/com/lidroid/xutils/cache/MD5FileNameGenerator.java
@@ -0,0 +1,38 @@
+package com.lidroid.xutils.cache;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Author: wyouflf
+ * Date: 14-5-16
+ * Time: 上午11:25
+ */
+public class MD5FileNameGenerator implements FileNameGenerator {
+ public MD5FileNameGenerator() {
+ }
+
+ public String generate(String key) {
+ String cacheKey;
+ try {
+ final MessageDigest mDigest = MessageDigest.getInstance("MD5");
+ mDigest.update(key.getBytes());
+ cacheKey = bytesToHexString(mDigest.digest());
+ } catch (NoSuchAlgorithmException e) {
+ cacheKey = String.valueOf(key.hashCode());
+ }
+ return cacheKey;
+ }
+
+ private String bytesToHexString(byte[] bytes) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < bytes.length; i++) {
+ String hex = Integer.toHexString(0xFF & bytes[i]);
+ if (hex.length() == 1) {
+ sb.append('0');
+ }
+ sb.append(hex);
+ }
+ return sb.toString();
+ }
+}
diff --git a/library/src/com/lidroid/xutils/db/annotation/Check.java b/library/src/com/lidroid/xutils/db/annotation/Check.java
new file mode 100644
index 0000000..8887c6a
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/annotation/Check.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2013. wyouflf (wyouflf@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.lidroid.xutils.db.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Author: wyouflf
+ * Date: 13-8-20
+ * Time: 上午9:44
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Check {
+ String value();
+}
diff --git a/src/com/lidroid/xutils/db/annotation/Column.java b/library/src/com/lidroid/xutils/db/annotation/Column.java
similarity index 91%
rename from src/com/lidroid/xutils/db/annotation/Column.java
rename to library/src/com/lidroid/xutils/db/annotation/Column.java
index 9df0a22..8f274c5 100644
--- a/src/com/lidroid/xutils/db/annotation/Column.java
+++ b/library/src/com/lidroid/xutils/db/annotation/Column.java
@@ -23,7 +23,8 @@
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
- public String column() default "";
- public String defaultValue() default "";
+ String column() default "";
+
+ String defaultValue() default "";
}
diff --git a/library/src/com/lidroid/xutils/db/annotation/Finder.java b/library/src/com/lidroid/xutils/db/annotation/Finder.java
new file mode 100644
index 0000000..e2f81ad
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/annotation/Finder.java
@@ -0,0 +1,20 @@
+package com.lidroid.xutils.db.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Author: wyouflf
+ * Date: 13-9-10
+ * Time: 下午6:44
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Finder {
+
+ String valueColumn();
+
+ String targetColumn();
+}
diff --git a/library/src/com/lidroid/xutils/db/annotation/Foreign.java b/library/src/com/lidroid/xutils/db/annotation/Foreign.java
new file mode 100644
index 0000000..4645b86
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/annotation/Foreign.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2013. wyouflf (wyouflf@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.lidroid.xutils.db.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Foreign {
+
+ String column() default "";
+
+ String foreign();
+}
diff --git a/src/com/lidroid/xutils/db/annotation/Id.java b/library/src/com/lidroid/xutils/db/annotation/Id.java
similarity index 95%
rename from src/com/lidroid/xutils/db/annotation/Id.java
rename to library/src/com/lidroid/xutils/db/annotation/Id.java
index 065387d..ea7b523 100644
--- a/src/com/lidroid/xutils/db/annotation/Id.java
+++ b/library/src/com/lidroid/xutils/db/annotation/Id.java
@@ -23,5 +23,5 @@
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Id {
- public String column() default "";
+ String column() default "";
}
diff --git a/library/src/com/lidroid/xutils/db/annotation/NoAutoIncrement.java b/library/src/com/lidroid/xutils/db/annotation/NoAutoIncrement.java
new file mode 100644
index 0000000..120e639
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/annotation/NoAutoIncrement.java
@@ -0,0 +1,16 @@
+package com.lidroid.xutils.db.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Author: wyouflf
+ * Date: 13-9-24
+ * Time: 上午9:33
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface NoAutoIncrement {
+}
diff --git a/library/src/com/lidroid/xutils/db/annotation/NotNull.java b/library/src/com/lidroid/xutils/db/annotation/NotNull.java
new file mode 100644
index 0000000..0485757
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/annotation/NotNull.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2013. wyouflf (wyouflf@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.lidroid.xutils.db.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Author: wyouflf
+ * Date: 13-8-20
+ * Time: 上午9:42
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface NotNull {
+}
diff --git a/src/com/lidroid/xutils/db/annotation/Table.java b/library/src/com/lidroid/xutils/db/annotation/Table.java
similarity index 92%
rename from src/com/lidroid/xutils/db/annotation/Table.java
rename to library/src/com/lidroid/xutils/db/annotation/Table.java
index 21316bf..55a8cc4 100644
--- a/src/com/lidroid/xutils/db/annotation/Table.java
+++ b/library/src/com/lidroid/xutils/db/annotation/Table.java
@@ -23,5 +23,8 @@
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
- public String name();
+
+ String name() default "";
+
+ String execAfterTableCreated() default "";
}
\ No newline at end of file
diff --git a/src/com/lidroid/xutils/db/annotation/Transient.java b/library/src/com/lidroid/xutils/db/annotation/Transient.java
similarity index 99%
rename from src/com/lidroid/xutils/db/annotation/Transient.java
rename to library/src/com/lidroid/xutils/db/annotation/Transient.java
index 7f06506..c1ba609 100644
--- a/src/com/lidroid/xutils/db/annotation/Transient.java
+++ b/library/src/com/lidroid/xutils/db/annotation/Transient.java
@@ -23,5 +23,4 @@
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Transient {
-
}
diff --git a/library/src/com/lidroid/xutils/db/annotation/Unique.java b/library/src/com/lidroid/xutils/db/annotation/Unique.java
new file mode 100644
index 0000000..5ed9b6a
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/annotation/Unique.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2013. wyouflf (wyouflf@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.lidroid.xutils.db.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Author: wyouflf
+ * Date: 13-8-20
+ * Time: 上午9:41
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Unique {
+}
diff --git a/library/src/com/lidroid/xutils/db/converter/BooleanColumnConverter.java b/library/src/com/lidroid/xutils/db/converter/BooleanColumnConverter.java
new file mode 100644
index 0000000..163ad78
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/converter/BooleanColumnConverter.java
@@ -0,0 +1,34 @@
+package com.lidroid.xutils.db.converter;
+
+import android.database.Cursor;
+import android.text.TextUtils;
+import com.lidroid.xutils.db.sqlite.ColumnDbType;
+
+/**
+ * Author: wyouflf
+ * Date: 13-11-4
+ * Time: 下午10:51
+ */
+public class BooleanColumnConverter implements ColumnConverter {
+ @Override
+ public Boolean getFieldValue(final Cursor cursor, int index) {
+ return cursor.isNull(index) ? null : cursor.getInt(index) == 1;
+ }
+
+ @Override
+ public Boolean getFieldValue(String fieldStringValue) {
+ if (TextUtils.isEmpty(fieldStringValue)) return null;
+ return fieldStringValue.length() == 1 ? "1".equals(fieldStringValue) : Boolean.valueOf(fieldStringValue);
+ }
+
+ @Override
+ public Object fieldValue2ColumnValue(Boolean fieldValue) {
+ if (fieldValue == null) return null;
+ return fieldValue ? 1 : 0;
+ }
+
+ @Override
+ public ColumnDbType getColumnDbType() {
+ return ColumnDbType.INTEGER;
+ }
+}
diff --git a/library/src/com/lidroid/xutils/db/converter/ByteArrayColumnConverter.java b/library/src/com/lidroid/xutils/db/converter/ByteArrayColumnConverter.java
new file mode 100644
index 0000000..d67cf34
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/converter/ByteArrayColumnConverter.java
@@ -0,0 +1,31 @@
+package com.lidroid.xutils.db.converter;
+
+import android.database.Cursor;
+import com.lidroid.xutils.db.sqlite.ColumnDbType;
+
+/**
+ * Author: wyouflf
+ * Date: 13-11-4
+ * Time: 下午10:51
+ */
+public class ByteArrayColumnConverter implements ColumnConverter {
+ @Override
+ public byte[] getFieldValue(final Cursor cursor, int index) {
+ return cursor.isNull(index) ? null : cursor.getBlob(index);
+ }
+
+ @Override
+ public byte[] getFieldValue(String fieldStringValue) {
+ return null;
+ }
+
+ @Override
+ public Object fieldValue2ColumnValue(byte[] fieldValue) {
+ return fieldValue;
+ }
+
+ @Override
+ public ColumnDbType getColumnDbType() {
+ return ColumnDbType.BLOB;
+ }
+}
diff --git a/library/src/com/lidroid/xutils/db/converter/ByteColumnConverter.java b/library/src/com/lidroid/xutils/db/converter/ByteColumnConverter.java
new file mode 100644
index 0000000..b70f37e
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/converter/ByteColumnConverter.java
@@ -0,0 +1,33 @@
+package com.lidroid.xutils.db.converter;
+
+import android.database.Cursor;
+import android.text.TextUtils;
+import com.lidroid.xutils.db.sqlite.ColumnDbType;
+
+/**
+ * Author: wyouflf
+ * Date: 13-11-4
+ * Time: 下午10:51
+ */
+public class ByteColumnConverter implements ColumnConverter {
+ @Override
+ public Byte getFieldValue(final Cursor cursor, int index) {
+ return cursor.isNull(index) ? null : (byte) cursor.getInt(index);
+ }
+
+ @Override
+ public Byte getFieldValue(String fieldStringValue) {
+ if (TextUtils.isEmpty(fieldStringValue)) return null;
+ return Byte.valueOf(fieldStringValue);
+ }
+
+ @Override
+ public Object fieldValue2ColumnValue(Byte fieldValue) {
+ return fieldValue;
+ }
+
+ @Override
+ public ColumnDbType getColumnDbType() {
+ return ColumnDbType.INTEGER;
+ }
+}
diff --git a/library/src/com/lidroid/xutils/db/converter/CharColumnConverter.java b/library/src/com/lidroid/xutils/db/converter/CharColumnConverter.java
new file mode 100644
index 0000000..53d8d72
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/converter/CharColumnConverter.java
@@ -0,0 +1,34 @@
+package com.lidroid.xutils.db.converter;
+
+import android.database.Cursor;
+import android.text.TextUtils;
+import com.lidroid.xutils.db.sqlite.ColumnDbType;
+
+/**
+ * Author: wyouflf
+ * Date: 13-11-4
+ * Time: 下午10:51
+ */
+public class CharColumnConverter implements ColumnConverter {
+ @Override
+ public Character getFieldValue(final Cursor cursor, int index) {
+ return cursor.isNull(index) ? null : (char) cursor.getInt(index);
+ }
+
+ @Override
+ public Character getFieldValue(String fieldStringValue) {
+ if (TextUtils.isEmpty(fieldStringValue)) return null;
+ return fieldStringValue.charAt(0);
+ }
+
+ @Override
+ public Object fieldValue2ColumnValue(Character fieldValue) {
+ if (fieldValue == null) return null;
+ return (int) fieldValue;
+ }
+
+ @Override
+ public ColumnDbType getColumnDbType() {
+ return ColumnDbType.INTEGER;
+ }
+}
diff --git a/library/src/com/lidroid/xutils/db/converter/ColumnConverter.java b/library/src/com/lidroid/xutils/db/converter/ColumnConverter.java
new file mode 100644
index 0000000..38b9842
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/converter/ColumnConverter.java
@@ -0,0 +1,20 @@
+package com.lidroid.xutils.db.converter;
+
+import android.database.Cursor;
+import com.lidroid.xutils.db.sqlite.ColumnDbType;
+
+/**
+ * Author: wyouflf
+ * Date: 13-11-4
+ * Time: 下午8:57
+ */
+public interface ColumnConverter {
+
+ T getFieldValue(final Cursor cursor, int index);
+
+ T getFieldValue(String fieldStringValue);
+
+ Object fieldValue2ColumnValue(T fieldValue);
+
+ ColumnDbType getColumnDbType();
+}
diff --git a/library/src/com/lidroid/xutils/db/converter/ColumnConverterFactory.java b/library/src/com/lidroid/xutils/db/converter/ColumnConverterFactory.java
new file mode 100644
index 0000000..c2e9172
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/converter/ColumnConverterFactory.java
@@ -0,0 +1,111 @@
+package com.lidroid.xutils.db.converter;
+
+import com.lidroid.xutils.db.sqlite.ColumnDbType;
+
+import java.util.Date;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Author: wyouflf
+ * Date: 13-11-4
+ * Time: 下午10:27
+ */
+public class ColumnConverterFactory {
+
+ private ColumnConverterFactory() {
+ }
+
+ public static ColumnConverter getColumnConverter(Class columnType) {
+ if (columnType_columnConverter_map.containsKey(columnType.getName())) {
+ return columnType_columnConverter_map.get(columnType.getName());
+ } else if (ColumnConverter.class.isAssignableFrom(columnType)) {
+ try {
+ ColumnConverter columnConverter = (ColumnConverter) columnType.newInstance();
+ if (columnConverter != null) {
+ columnType_columnConverter_map.put(columnType.getName(), columnConverter);
+ }
+ return columnConverter;
+ } catch (Throwable e) {
+ }
+ }
+ return null;
+ }
+
+ public static ColumnDbType getDbColumnType(Class columnType) {
+ ColumnConverter converter = getColumnConverter(columnType);
+ if (converter != null) {
+ return converter.getColumnDbType();
+ }
+ return ColumnDbType.TEXT;
+ }
+
+ public static void registerColumnConverter(Class columnType, ColumnConverter columnConverter) {
+ columnType_columnConverter_map.put(columnType.getName(), columnConverter);
+ }
+
+ public static boolean isSupportColumnConverter(Class columnType) {
+ if (columnType_columnConverter_map.containsKey(columnType.getName())) {
+ return true;
+ } else if (ColumnConverter.class.isAssignableFrom(columnType)) {
+ try {
+ ColumnConverter columnConverter = (ColumnConverter) columnType.newInstance();
+ if (columnConverter != null) {
+ columnType_columnConverter_map.put(columnType.getName(), columnConverter);
+ }
+ return columnConverter == null;
+ } catch (Throwable e) {
+ }
+ }
+ return false;
+ }
+
+ private static final ConcurrentHashMap columnType_columnConverter_map;
+
+ static {
+ columnType_columnConverter_map = new ConcurrentHashMap();
+
+ BooleanColumnConverter booleanColumnConverter = new BooleanColumnConverter();
+ columnType_columnConverter_map.put(boolean.class.getName(), booleanColumnConverter);
+ columnType_columnConverter_map.put(Boolean.class.getName(), booleanColumnConverter);
+
+ ByteArrayColumnConverter byteArrayColumnConverter = new ByteArrayColumnConverter();
+ columnType_columnConverter_map.put(byte[].class.getName(), byteArrayColumnConverter);
+
+ ByteColumnConverter byteColumnConverter = new ByteColumnConverter();
+ columnType_columnConverter_map.put(byte.class.getName(), byteColumnConverter);
+ columnType_columnConverter_map.put(Byte.class.getName(), byteColumnConverter);
+
+ CharColumnConverter charColumnConverter = new CharColumnConverter();
+ columnType_columnConverter_map.put(char.class.getName(), charColumnConverter);
+ columnType_columnConverter_map.put(Character.class.getName(), charColumnConverter);
+
+ DateColumnConverter dateColumnConverter = new DateColumnConverter();
+ columnType_columnConverter_map.put(Date.class.getName(), dateColumnConverter);
+
+ DoubleColumnConverter doubleColumnConverter = new DoubleColumnConverter();
+ columnType_columnConverter_map.put(double.class.getName(), doubleColumnConverter);
+ columnType_columnConverter_map.put(Double.class.getName(), doubleColumnConverter);
+
+ FloatColumnConverter floatColumnConverter = new FloatColumnConverter();
+ columnType_columnConverter_map.put(float.class.getName(), floatColumnConverter);
+ columnType_columnConverter_map.put(Float.class.getName(), floatColumnConverter);
+
+ IntegerColumnConverter integerColumnConverter = new IntegerColumnConverter();
+ columnType_columnConverter_map.put(int.class.getName(), integerColumnConverter);
+ columnType_columnConverter_map.put(Integer.class.getName(), integerColumnConverter);
+
+ LongColumnConverter longColumnConverter = new LongColumnConverter();
+ columnType_columnConverter_map.put(long.class.getName(), longColumnConverter);
+ columnType_columnConverter_map.put(Long.class.getName(), longColumnConverter);
+
+ ShortColumnConverter shortColumnConverter = new ShortColumnConverter();
+ columnType_columnConverter_map.put(short.class.getName(), shortColumnConverter);
+ columnType_columnConverter_map.put(Short.class.getName(), shortColumnConverter);
+
+ SqlDateColumnConverter sqlDateColumnConverter = new SqlDateColumnConverter();
+ columnType_columnConverter_map.put(java.sql.Date.class.getName(), sqlDateColumnConverter);
+
+ StringColumnConverter stringColumnConverter = new StringColumnConverter();
+ columnType_columnConverter_map.put(String.class.getName(), stringColumnConverter);
+ }
+}
diff --git a/library/src/com/lidroid/xutils/db/converter/DateColumnConverter.java b/library/src/com/lidroid/xutils/db/converter/DateColumnConverter.java
new file mode 100644
index 0000000..d42f748
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/converter/DateColumnConverter.java
@@ -0,0 +1,36 @@
+package com.lidroid.xutils.db.converter;
+
+import android.database.Cursor;
+import android.text.TextUtils;
+import com.lidroid.xutils.db.sqlite.ColumnDbType;
+
+import java.util.Date;
+
+/**
+ * Author: wyouflf
+ * Date: 13-11-4
+ * Time: 下午10:51
+ */
+public class DateColumnConverter implements ColumnConverter {
+ @Override
+ public Date getFieldValue(final Cursor cursor, int index) {
+ return cursor.isNull(index) ? null : new Date(cursor.getLong(index));
+ }
+
+ @Override
+ public Date getFieldValue(String fieldStringValue) {
+ if (TextUtils.isEmpty(fieldStringValue)) return null;
+ return new Date(Long.valueOf(fieldStringValue));
+ }
+
+ @Override
+ public Object fieldValue2ColumnValue(Date fieldValue) {
+ if (fieldValue == null) return null;
+ return fieldValue.getTime();
+ }
+
+ @Override
+ public ColumnDbType getColumnDbType() {
+ return ColumnDbType.INTEGER;
+ }
+}
diff --git a/library/src/com/lidroid/xutils/db/converter/DoubleColumnConverter.java b/library/src/com/lidroid/xutils/db/converter/DoubleColumnConverter.java
new file mode 100644
index 0000000..fcfc118
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/converter/DoubleColumnConverter.java
@@ -0,0 +1,33 @@
+package com.lidroid.xutils.db.converter;
+
+import android.database.Cursor;
+import android.text.TextUtils;
+import com.lidroid.xutils.db.sqlite.ColumnDbType;
+
+/**
+ * Author: wyouflf
+ * Date: 13-11-4
+ * Time: 下午10:51
+ */
+public class DoubleColumnConverter implements ColumnConverter {
+ @Override
+ public Double getFieldValue(final Cursor cursor, int index) {
+ return cursor.isNull(index) ? null : cursor.getDouble(index);
+ }
+
+ @Override
+ public Double getFieldValue(String fieldStringValue) {
+ if (TextUtils.isEmpty(fieldStringValue)) return null;
+ return Double.valueOf(fieldStringValue);
+ }
+
+ @Override
+ public Object fieldValue2ColumnValue(Double fieldValue) {
+ return fieldValue;
+ }
+
+ @Override
+ public ColumnDbType getColumnDbType() {
+ return ColumnDbType.REAL;
+ }
+}
diff --git a/library/src/com/lidroid/xutils/db/converter/FloatColumnConverter.java b/library/src/com/lidroid/xutils/db/converter/FloatColumnConverter.java
new file mode 100644
index 0000000..cf37a5b
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/converter/FloatColumnConverter.java
@@ -0,0 +1,33 @@
+package com.lidroid.xutils.db.converter;
+
+import android.database.Cursor;
+import android.text.TextUtils;
+import com.lidroid.xutils.db.sqlite.ColumnDbType;
+
+/**
+ * Author: wyouflf
+ * Date: 13-11-4
+ * Time: 下午10:51
+ */
+public class FloatColumnConverter implements ColumnConverter {
+ @Override
+ public Float getFieldValue(final Cursor cursor, int index) {
+ return cursor.isNull(index) ? null : cursor.getFloat(index);
+ }
+
+ @Override
+ public Float getFieldValue(String fieldStringValue) {
+ if (TextUtils.isEmpty(fieldStringValue)) return null;
+ return Float.valueOf(fieldStringValue);
+ }
+
+ @Override
+ public Object fieldValue2ColumnValue(Float fieldValue) {
+ return fieldValue;
+ }
+
+ @Override
+ public ColumnDbType getColumnDbType() {
+ return ColumnDbType.REAL;
+ }
+}
diff --git a/library/src/com/lidroid/xutils/db/converter/IntegerColumnConverter.java b/library/src/com/lidroid/xutils/db/converter/IntegerColumnConverter.java
new file mode 100644
index 0000000..a2ea02d
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/converter/IntegerColumnConverter.java
@@ -0,0 +1,33 @@
+package com.lidroid.xutils.db.converter;
+
+import android.database.Cursor;
+import android.text.TextUtils;
+import com.lidroid.xutils.db.sqlite.ColumnDbType;
+
+/**
+ * Author: wyouflf
+ * Date: 13-11-4
+ * Time: 下午10:51
+ */
+public class IntegerColumnConverter implements ColumnConverter {
+ @Override
+ public Integer getFieldValue(final Cursor cursor, int index) {
+ return cursor.isNull(index) ? null : cursor.getInt(index);
+ }
+
+ @Override
+ public Integer getFieldValue(String fieldStringValue) {
+ if (TextUtils.isEmpty(fieldStringValue)) return null;
+ return Integer.valueOf(fieldStringValue);
+ }
+
+ @Override
+ public Object fieldValue2ColumnValue(Integer fieldValue) {
+ return fieldValue;
+ }
+
+ @Override
+ public ColumnDbType getColumnDbType() {
+ return ColumnDbType.INTEGER;
+ }
+}
diff --git a/library/src/com/lidroid/xutils/db/converter/LongColumnConverter.java b/library/src/com/lidroid/xutils/db/converter/LongColumnConverter.java
new file mode 100644
index 0000000..f031ec3
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/converter/LongColumnConverter.java
@@ -0,0 +1,33 @@
+package com.lidroid.xutils.db.converter;
+
+import android.database.Cursor;
+import android.text.TextUtils;
+import com.lidroid.xutils.db.sqlite.ColumnDbType;
+
+/**
+ * Author: wyouflf
+ * Date: 13-11-4
+ * Time: 下午10:51
+ */
+public class LongColumnConverter implements ColumnConverter {
+ @Override
+ public Long getFieldValue(final Cursor cursor, int index) {
+ return cursor.isNull(index) ? null : cursor.getLong(index);
+ }
+
+ @Override
+ public Long getFieldValue(String fieldStringValue) {
+ if (TextUtils.isEmpty(fieldStringValue)) return null;
+ return Long.valueOf(fieldStringValue);
+ }
+
+ @Override
+ public Object fieldValue2ColumnValue(Long fieldValue) {
+ return fieldValue;
+ }
+
+ @Override
+ public ColumnDbType getColumnDbType() {
+ return ColumnDbType.INTEGER;
+ }
+}
diff --git a/library/src/com/lidroid/xutils/db/converter/ShortColumnConverter.java b/library/src/com/lidroid/xutils/db/converter/ShortColumnConverter.java
new file mode 100644
index 0000000..48e61cc
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/converter/ShortColumnConverter.java
@@ -0,0 +1,33 @@
+package com.lidroid.xutils.db.converter;
+
+import android.database.Cursor;
+import android.text.TextUtils;
+import com.lidroid.xutils.db.sqlite.ColumnDbType;
+
+/**
+ * Author: wyouflf
+ * Date: 13-11-4
+ * Time: 下午10:51
+ */
+public class ShortColumnConverter implements ColumnConverter {
+ @Override
+ public Short getFieldValue(final Cursor cursor, int index) {
+ return cursor.isNull(index) ? null : cursor.getShort(index);
+ }
+
+ @Override
+ public Short getFieldValue(String fieldStringValue) {
+ if (TextUtils.isEmpty(fieldStringValue)) return null;
+ return Short.valueOf(fieldStringValue);
+ }
+
+ @Override
+ public Object fieldValue2ColumnValue(Short fieldValue) {
+ return fieldValue;
+ }
+
+ @Override
+ public ColumnDbType getColumnDbType() {
+ return ColumnDbType.INTEGER;
+ }
+}
diff --git a/library/src/com/lidroid/xutils/db/converter/SqlDateColumnConverter.java b/library/src/com/lidroid/xutils/db/converter/SqlDateColumnConverter.java
new file mode 100644
index 0000000..472b1c7
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/converter/SqlDateColumnConverter.java
@@ -0,0 +1,34 @@
+package com.lidroid.xutils.db.converter;
+
+import android.database.Cursor;
+import android.text.TextUtils;
+import com.lidroid.xutils.db.sqlite.ColumnDbType;
+
+/**
+ * Author: wyouflf
+ * Date: 13-11-4
+ * Time: 下午10:51
+ */
+public class SqlDateColumnConverter implements ColumnConverter {
+ @Override
+ public java.sql.Date getFieldValue(final Cursor cursor, int index) {
+ return cursor.isNull(index) ? null : new java.sql.Date(cursor.getLong(index));
+ }
+
+ @Override
+ public java.sql.Date getFieldValue(String fieldStringValue) {
+ if (TextUtils.isEmpty(fieldStringValue)) return null;
+ return new java.sql.Date(Long.valueOf(fieldStringValue));
+ }
+
+ @Override
+ public Object fieldValue2ColumnValue(java.sql.Date fieldValue) {
+ if (fieldValue == null) return null;
+ return fieldValue.getTime();
+ }
+
+ @Override
+ public ColumnDbType getColumnDbType() {
+ return ColumnDbType.INTEGER;
+ }
+}
diff --git a/library/src/com/lidroid/xutils/db/converter/StringColumnConverter.java b/library/src/com/lidroid/xutils/db/converter/StringColumnConverter.java
new file mode 100644
index 0000000..b6960f1
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/converter/StringColumnConverter.java
@@ -0,0 +1,31 @@
+package com.lidroid.xutils.db.converter;
+
+import android.database.Cursor;
+import com.lidroid.xutils.db.sqlite.ColumnDbType;
+
+/**
+ * Author: wyouflf
+ * Date: 13-11-4
+ * Time: 下午10:51
+ */
+public class StringColumnConverter implements ColumnConverter {
+ @Override
+ public String getFieldValue(final Cursor cursor, int index) {
+ return cursor.isNull(index) ? null : cursor.getString(index);
+ }
+
+ @Override
+ public String getFieldValue(String fieldStringValue) {
+ return fieldStringValue;
+ }
+
+ @Override
+ public Object fieldValue2ColumnValue(String fieldValue) {
+ return fieldValue;
+ }
+
+ @Override
+ public ColumnDbType getColumnDbType() {
+ return ColumnDbType.TEXT;
+ }
+}
diff --git a/library/src/com/lidroid/xutils/db/sqlite/ColumnDbType.java b/library/src/com/lidroid/xutils/db/sqlite/ColumnDbType.java
new file mode 100644
index 0000000..140fba9
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/sqlite/ColumnDbType.java
@@ -0,0 +1,20 @@
+package com.lidroid.xutils.db.sqlite;
+
+/**
+ * Created by wyouflf on 14-2-20.
+ */
+public enum ColumnDbType {
+
+ INTEGER("INTEGER"), REAL("REAL"), TEXT("TEXT"), BLOB("BLOB");
+
+ private String value;
+
+ ColumnDbType(String value) {
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+}
diff --git a/library/src/com/lidroid/xutils/db/sqlite/CursorUtils.java b/library/src/com/lidroid/xutils/db/sqlite/CursorUtils.java
new file mode 100644
index 0000000..40a3e11
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/sqlite/CursorUtils.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2013. wyouflf (wyouflf@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.lidroid.xutils.db.sqlite;
+
+import android.database.Cursor;
+import com.lidroid.xutils.DbUtils;
+import com.lidroid.xutils.db.table.*;
+import com.lidroid.xutils.util.LogUtils;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+public class CursorUtils {
+
+ @SuppressWarnings("unchecked")
+ public static T getEntity(final DbUtils db, final Cursor cursor, Class entityType, long findCacheSequence) {
+ if (db == null || cursor == null) return null;
+
+ EntityTempCache.setSeq(findCacheSequence);
+ try {
+ Table table = Table.get(db, entityType);
+ Id id = table.id;
+ String idColumnName = id.getColumnName();
+ int idIndex = id.getIndex();
+ if (idIndex < 0) {
+ idIndex = cursor.getColumnIndex(idColumnName);
+ }
+ Object idValue = id.getColumnConverter().getFieldValue(cursor, idIndex);
+ T entity = EntityTempCache.get(entityType, idValue);
+ if (entity == null) {
+ entity = entityType.newInstance();
+ id.setValue2Entity(entity, cursor, idIndex);
+ EntityTempCache.put(entityType, idValue, entity);
+ } else {
+ return entity;
+ }
+ int columnCount = cursor.getColumnCount();
+ for (int i = 0; i < columnCount; i++) {
+ String columnName = cursor.getColumnName(i);
+ Column column = table.columnMap.get(columnName);
+ if (column != null) {
+ column.setValue2Entity(entity, cursor, i);
+ }
+ }
+
+ // init finder
+ for (Finder finder : table.finderMap.values()) {
+ finder.setValue2Entity(entity, null, 0);
+ }
+ return entity;
+ } catch (Throwable e) {
+ LogUtils.e(e.getMessage(), e);
+ }
+
+ return null;
+ }
+
+ public static DbModel getDbModel(final Cursor cursor) {
+ DbModel result = null;
+ if (cursor != null) {
+ result = new DbModel();
+ int columnCount = cursor.getColumnCount();
+ for (int i = 0; i < columnCount; i++) {
+ result.add(cursor.getColumnName(i), cursor.getString(i));
+ }
+ }
+ return result;
+ }
+
+ public static class FindCacheSequence {
+ private FindCacheSequence() {
+ }
+
+ private static long seq = 0;
+ private static final String FOREIGN_LAZY_LOADER_CLASS_NAME = ForeignLazyLoader.class.getName();
+ private static final String FINDER_LAZY_LOADER_CLASS_NAME = FinderLazyLoader.class.getName();
+
+ public static long getSeq() {
+ String findMethodCaller = Thread.currentThread().getStackTrace()[4].getClassName();
+ if (!findMethodCaller.equals(FOREIGN_LAZY_LOADER_CLASS_NAME) && !findMethodCaller.equals(FINDER_LAZY_LOADER_CLASS_NAME)) {
+ ++seq;
+ }
+ return seq;
+ }
+ }
+
+ private static class EntityTempCache {
+ private EntityTempCache() {
+ }
+
+ private static final ConcurrentHashMap cache = new ConcurrentHashMap();
+
+ private static long seq = 0;
+
+ public static void put(Class entityType, Object idValue, Object entity) {
+ cache.put(entityType.getName() + "#" + idValue, entity);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static T get(Class entityType, Object idValue) {
+ return (T) cache.get(entityType.getName() + "#" + idValue);
+ }
+
+ public static void setSeq(long seq) {
+ if (EntityTempCache.seq != seq) {
+ cache.clear();
+ EntityTempCache.seq = seq;
+ }
+ }
+ }
+}
diff --git a/library/src/com/lidroid/xutils/db/sqlite/DbModelSelector.java b/library/src/com/lidroid/xutils/db/sqlite/DbModelSelector.java
new file mode 100644
index 0000000..3d28861
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/sqlite/DbModelSelector.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (c) 2013. wyouflf (wyouflf@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.lidroid.xutils.db.sqlite;
+
+import android.text.TextUtils;
+
+/**
+ * Author: wyouflf
+ * Date: 13-8-10
+ * Time: 下午2:15
+ */
+public class DbModelSelector {
+
+ private String[] columnExpressions;
+ private String groupByColumnName;
+ private WhereBuilder having;
+
+ private Selector selector;
+
+ private DbModelSelector(Class> entityType) {
+ selector = Selector.from(entityType);
+ }
+
+ protected DbModelSelector(Selector selector, String groupByColumnName) {
+ this.selector = selector;
+ this.groupByColumnName = groupByColumnName;
+ }
+
+ protected DbModelSelector(Selector selector, String[] columnExpressions) {
+ this.selector = selector;
+ this.columnExpressions = columnExpressions;
+ }
+
+ public static DbModelSelector from(Class> entityType) {
+ return new DbModelSelector(entityType);
+ }
+
+ public DbModelSelector where(WhereBuilder whereBuilder) {
+ selector.where(whereBuilder);
+ return this;
+ }
+
+ public DbModelSelector where(String columnName, String op, Object value) {
+ selector.where(columnName, op, value);
+ return this;
+ }
+
+ public DbModelSelector and(String columnName, String op, Object value) {
+ selector.and(columnName, op, value);
+ return this;
+ }
+
+ public DbModelSelector and(WhereBuilder where) {
+ selector.and(where);
+ return this;
+ }
+
+ public DbModelSelector or(String columnName, String op, Object value) {
+ selector.or(columnName, op, value);
+ return this;
+ }
+
+ public DbModelSelector or(WhereBuilder where) {
+ selector.or(where);
+ return this;
+ }
+
+ public DbModelSelector expr(String expr) {
+ selector.expr(expr);
+ return this;
+ }
+
+ public DbModelSelector expr(String columnName, String op, Object value) {
+ selector.expr(columnName, op, value);
+ return this;
+ }
+
+ public DbModelSelector groupBy(String columnName) {
+ this.groupByColumnName = columnName;
+ return this;
+ }
+
+ public DbModelSelector having(WhereBuilder whereBuilder) {
+ this.having = whereBuilder;
+ return this;
+ }
+
+ public DbModelSelector select(String... columnExpressions) {
+ this.columnExpressions = columnExpressions;
+ return this;
+ }
+
+ public DbModelSelector orderBy(String columnName) {
+ selector.orderBy(columnName);
+ return this;
+ }
+
+ public DbModelSelector orderBy(String columnName, boolean desc) {
+ selector.orderBy(columnName, desc);
+ return this;
+ }
+
+ public DbModelSelector limit(int limit) {
+ selector.limit(limit);
+ return this;
+ }
+
+ public DbModelSelector offset(int offset) {
+ selector.offset(offset);
+ return this;
+ }
+
+ public Class> getEntityType() {
+ return selector.getEntityType();
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer result = new StringBuffer();
+ result.append("SELECT ");
+ if (columnExpressions != null && columnExpressions.length > 0) {
+ for (int i = 0; i < columnExpressions.length; i++) {
+ result.append(columnExpressions[i]);
+ result.append(",");
+ }
+ result.deleteCharAt(result.length() - 1);
+ } else {
+ if (!TextUtils.isEmpty(groupByColumnName)) {
+ result.append(groupByColumnName);
+ } else {
+ result.append("*");
+ }
+ }
+ result.append(" FROM ").append(selector.tableName);
+ if (selector.whereBuilder != null && selector.whereBuilder.getWhereItemSize() > 0) {
+ result.append(" WHERE ").append(selector.whereBuilder.toString());
+ }
+ if (!TextUtils.isEmpty(groupByColumnName)) {
+ result.append(" GROUP BY ").append(groupByColumnName);
+ if (having != null && having.getWhereItemSize() > 0) {
+ result.append(" HAVING ").append(having.toString());
+ }
+ }
+ if (selector.orderByList != null) {
+ for (int i = 0; i < selector.orderByList.size(); i++) {
+ result.append(" ORDER BY ").append(selector.orderByList.get(i).toString());
+ }
+ }
+ if (selector.limit > 0) {
+ result.append(" LIMIT ").append(selector.limit);
+ result.append(" OFFSET ").append(selector.offset);
+ }
+ return result.toString();
+ }
+}
diff --git a/library/src/com/lidroid/xutils/db/sqlite/FinderLazyLoader.java b/library/src/com/lidroid/xutils/db/sqlite/FinderLazyLoader.java
new file mode 100644
index 0000000..a748c5b
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/sqlite/FinderLazyLoader.java
@@ -0,0 +1,47 @@
+package com.lidroid.xutils.db.sqlite;
+
+import com.lidroid.xutils.db.table.ColumnUtils;
+import com.lidroid.xutils.db.table.Finder;
+import com.lidroid.xutils.db.table.Table;
+import com.lidroid.xutils.exception.DbException;
+
+import java.util.List;
+
+/**
+ * Author: wyouflf
+ * Date: 13-9-10
+ * Time: 下午10:50
+ */
+public class FinderLazyLoader {
+ private final Finder finderColumn;
+ private final Object finderValue;
+
+ public FinderLazyLoader(Finder finderColumn, Object value) {
+ this.finderColumn = finderColumn;
+ this.finderValue = ColumnUtils.convert2DbColumnValueIfNeeded(value);
+ }
+
+ public List getAllFromDb() throws DbException {
+ List entities = null;
+ Table table = finderColumn.getTable();
+ if (table != null) {
+ entities = table.db.findAll(
+ Selector.from(finderColumn.getTargetEntityType()).
+ where(finderColumn.getTargetColumnName(), "=", finderValue)
+ );
+ }
+ return entities;
+ }
+
+ public T getFirstFromDb() throws DbException {
+ T entity = null;
+ Table table = finderColumn.getTable();
+ if (table != null) {
+ entity = table.db.findFirst(
+ Selector.from(finderColumn.getTargetEntityType()).
+ where(finderColumn.getTargetColumnName(), "=", finderValue)
+ );
+ }
+ return entity;
+ }
+}
diff --git a/library/src/com/lidroid/xutils/db/sqlite/ForeignLazyLoader.java b/library/src/com/lidroid/xutils/db/sqlite/ForeignLazyLoader.java
new file mode 100644
index 0000000..2c34453
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/sqlite/ForeignLazyLoader.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2013. wyouflf (wyouflf@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.lidroid.xutils.db.sqlite;
+
+import com.lidroid.xutils.db.table.ColumnUtils;
+import com.lidroid.xutils.db.table.Foreign;
+import com.lidroid.xutils.db.table.Table;
+import com.lidroid.xutils.exception.DbException;
+
+import java.util.List;
+
+public class ForeignLazyLoader {
+ private final Foreign foreignColumn;
+ private Object columnValue;
+
+ public ForeignLazyLoader(Foreign foreignColumn, Object value) {
+ this.foreignColumn = foreignColumn;
+ this.columnValue = ColumnUtils.convert2DbColumnValueIfNeeded(value);
+ }
+
+ public List getAllFromDb() throws DbException {
+ List entities = null;
+ Table table = foreignColumn.getTable();
+ if (table != null) {
+ entities = table.db.findAll(
+ Selector.from(foreignColumn.getForeignEntityType()).
+ where(foreignColumn.getForeignColumnName(), "=", columnValue)
+ );
+ }
+ return entities;
+ }
+
+ public T getFirstFromDb() throws DbException {
+ T entity = null;
+ Table table = foreignColumn.getTable();
+ if (table != null) {
+ entity = table.db.findFirst(
+ Selector.from(foreignColumn.getForeignEntityType()).
+ where(foreignColumn.getForeignColumnName(), "=", columnValue)
+ );
+ }
+ return entity;
+ }
+
+ public void setColumnValue(Object value) {
+ this.columnValue = ColumnUtils.convert2DbColumnValueIfNeeded(value);
+ }
+
+ public Object getColumnValue() {
+ return columnValue;
+ }
+}
diff --git a/library/src/com/lidroid/xutils/db/sqlite/Selector.java b/library/src/com/lidroid/xutils/db/sqlite/Selector.java
new file mode 100644
index 0000000..422ab47
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/sqlite/Selector.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (c) 2013. wyouflf (wyouflf@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.lidroid.xutils.db.sqlite;
+
+import com.lidroid.xutils.db.table.TableUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Author: wyouflf
+ * Date: 13-8-9
+ * Time: 下午10:19
+ */
+public class Selector {
+
+ protected Class> entityType;
+ protected String tableName;
+
+ protected WhereBuilder whereBuilder;
+ protected List orderByList;
+ protected int limit = 0;
+ protected int offset = 0;
+
+ private Selector(Class> entityType) {
+ this.entityType = entityType;
+ this.tableName = TableUtils.getTableName(entityType);
+ }
+
+ public static Selector from(Class> entityType) {
+ return new Selector(entityType);
+ }
+
+ public Selector where(WhereBuilder whereBuilder) {
+ this.whereBuilder = whereBuilder;
+ return this;
+ }
+
+ public Selector where(String columnName, String op, Object value) {
+ this.whereBuilder = WhereBuilder.b(columnName, op, value);
+ return this;
+ }
+
+ public Selector and(String columnName, String op, Object value) {
+ this.whereBuilder.and(columnName, op, value);
+ return this;
+ }
+
+ public Selector and(WhereBuilder where) {
+ this.whereBuilder.expr("AND (" + where.toString() + ")");
+ return this;
+ }
+
+ public Selector or(String columnName, String op, Object value) {
+ this.whereBuilder.or(columnName, op, value);
+ return this;
+ }
+
+ public Selector or(WhereBuilder where) {
+ this.whereBuilder.expr("OR (" + where.toString() + ")");
+ return this;
+ }
+
+ public Selector expr(String expr) {
+ if (this.whereBuilder == null) {
+ this.whereBuilder = WhereBuilder.b();
+ }
+ this.whereBuilder.expr(expr);
+ return this;
+ }
+
+ public Selector expr(String columnName, String op, Object value) {
+ if (this.whereBuilder == null) {
+ this.whereBuilder = WhereBuilder.b();
+ }
+ this.whereBuilder.expr(columnName, op, value);
+ return this;
+ }
+
+ public DbModelSelector groupBy(String columnName) {
+ return new DbModelSelector(this, columnName);
+ }
+
+ public DbModelSelector select(String... columnExpressions) {
+ return new DbModelSelector(this, columnExpressions);
+ }
+
+ public Selector orderBy(String columnName) {
+ if (orderByList == null) {
+ orderByList = new ArrayList(2);
+ }
+ orderByList.add(new OrderBy(columnName));
+ return this;
+ }
+
+ public Selector orderBy(String columnName, boolean desc) {
+ if (orderByList == null) {
+ orderByList = new ArrayList(2);
+ }
+ orderByList.add(new OrderBy(columnName, desc));
+ return this;
+ }
+
+ public Selector limit(int limit) {
+ this.limit = limit;
+ return this;
+ }
+
+ public Selector offset(int offset) {
+ this.offset = offset;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("SELECT ");
+ result.append("*");
+ result.append(" FROM ").append(tableName);
+ if (whereBuilder != null && whereBuilder.getWhereItemSize() > 0) {
+ result.append(" WHERE ").append(whereBuilder.toString());
+ }
+ if (orderByList != null) {
+ for (int i = 0; i < orderByList.size(); i++) {
+ result.append(" ORDER BY ").append(orderByList.get(i).toString());
+ }
+ }
+ if (limit > 0) {
+ result.append(" LIMIT ").append(limit);
+ result.append(" OFFSET ").append(offset);
+ }
+ return result.toString();
+ }
+
+ public Class> getEntityType() {
+ return entityType;
+ }
+
+ protected class OrderBy {
+ private String columnName;
+ private boolean desc;
+
+ public OrderBy(String columnName) {
+ this.columnName = columnName;
+ }
+
+ public OrderBy(String columnName, boolean desc) {
+ this.columnName = columnName;
+ this.desc = desc;
+ }
+
+ @Override
+ public String toString() {
+ return columnName + (desc ? " DESC" : " ASC");
+ }
+ }
+}
diff --git a/library/src/com/lidroid/xutils/db/sqlite/SqlInfo.java b/library/src/com/lidroid/xutils/db/sqlite/SqlInfo.java
new file mode 100644
index 0000000..77f031c
--- /dev/null
+++ b/library/src/com/lidroid/xutils/db/sqlite/SqlInfo.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2013. wyouflf (wyouflf@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.lidroid.xutils.db.sqlite;
+
+import com.lidroid.xutils.db.table.ColumnUtils;
+
+import java.util.LinkedList;
+
+public class SqlInfo {
+
+ private String sql;
+ private LinkedList