/* * Copyright 2015 Daniel Dittmar * * 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 dan.dit.whatsthat.image; import android.content.Context; import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; import android.util.Xml; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import dan.dit.whatsthat.preferences.Tongue; import dan.dit.whatsthat.riddle.types.RiddleType; import dan.dit.whatsthat.solution.Solution; import dan.dit.whatsthat.storage.ImageTable; import dan.dit.whatsthat.util.general.BuildException; import dan.dit.whatsthat.util.general.PercentProgressListener; import dan.dit.whatsthat.util.image.BitmapUtil; /** * XML for easily initializing and loading new images into the app. Format: * <imagedata> * <bundle version="1"> * <image> * <hash></hash> * <resname></resname> * <solutions> * <solution> * <tongue></tongue> * <word></word> * <word></word> * </solution> * <solution> * <tongue></tongue> * <word></word> * </solution> * </solutions> * <author> * <name></name> * <source></source> * <license></license> * <title></title> * <extras></extras> * </author> * <riddleprefs> * <type></type> * <type></type> * </riddleprefs> * <riddlerefused> * <type></type> * <type></type> * </riddlerefused> * </image> * <image> * ... * </image> * </bundle> * <bundle version="2"> * ... * </bundle> * </imagedata> * Created by daniel on 18.04.15. */ public class ImageXmlParser { // We don't use namespaces public static final String NAMESPACE = null; public static final String TAG_ALL_BUNDLES_NAME = "imagedata"; public static final String TAG_BUNDLE_NAME = "bundle"; public static final String TAG_IMAGE_NAME = "image"; public static final String BUNDLE_ATTRIBUTE_VERSION_NAME = "version"; public static final String TAG_SOLUTION_NAME = "solution"; public static final String TAG_SOLUTION_TONGUE_NAME = "tongue"; public static final String TAG_SOLUTION_WORD_NAME = "word"; public static final String TAG_AUTHOR_NAME = "name"; public static final String TAG_AUTHOR_SOURCE = "source"; public static final String TAG_AUTHOR_LICENSE = "license"; public static final String TAG_AUTHOR_TITLE = "title"; public static final String TAG_AUTHOR_EXTRAS = "extras"; public static final String TAG_RIDDLE_TYPE_NAME = "type"; private static final String BUNDLE_ATTRIBUTE_ORIGIN = "origin"; private Context mContext; private int mHighestReadBundleNumber = Integer.MIN_VALUE; private SparseArray<List<Image>> mReadBundles = new SparseArray<>(); private SparseArray<String> mReadBundlesOrigin = new SparseArray<>(); private boolean mModeAbortOnImageBuildFailure; private BitmapUtil.ByteBufferHolder mBuffer = new BitmapUtil.ByteBufferHolder(); private String mCurrOrigin; public List<Image> getBundle(int bundleNumber) { return mReadBundles.get(bundleNumber); } public Set<Integer> getReadBundleNumbers() { Set<Integer> keys = new HashSet<>(mReadBundles.size()); for (int i = 0; i < mReadBundles.size(); i++) { keys.add(mReadBundles.keyAt(i)); } return keys; } private ImageXmlParser() {} /** * Parses all new bundles found in the given stream (xml file). * @param context The context. * @param inputStream The input to parse. * @param startBundleNumber The bundle version number to start parsing (useful to skip old bundles when updating). * @param abortOnFailure If true the parser will stop and abort with an Exception if there was an error building an image. * @return The parser or null if the inputStream or context was null. * @throws XmlPullParserException There was an XML error parsing the data. * @throws IOException There was an IO error reading the data. */ public static ImageXmlParser parseInput(Context context, InputStream inputStream, int startBundleNumber, boolean abortOnFailure) throws XmlPullParserException, IOException { if (inputStream == null || context == null) { return null; } ImageXmlParser parser = new ImageXmlParser(); parser.mContext = context; parser.mModeAbortOnImageBuildFailure = abortOnFailure; parser.parse(inputStream, startBundleNumber); Log.d("Image", "Parsed new bundles: Loaded images from XML with highest read number= " + parser.mHighestReadBundleNumber); return parser; } public boolean syncToDatabase(ImageManager.SynchronizationListener listener) { if (mReadBundles == null || mReadBundles.size() == 0) { return false; } List<Integer> keyList = new ArrayList<>(getReadBundleNumbers()); Collections.sort(keyList); // ascending order so that higher version bundles can overwrite older images int imageCount = 0; for (Integer k : keyList) { imageCount += mReadBundles.get(k).size(); } if (imageCount > 0) { double progress = 0; double parseProgressPerImage = PercentProgressListener.PROGRESS_COMPLETE / (double) imageCount; for (Integer version : keyList) { for (Image img : mReadBundles.get(version)) { if (listener != null && listener.isSyncCancelled()) { return false; } img.saveToDatabase(mContext); progress += parseProgressPerImage; postProgress((int) progress, listener); } } Log.d("Image", "Synced " + imageCount + " images to database from " + getReadBundlesCount() + " read bundles."); return true; } return false; } private void postProgress(int progress, ImageManager.SynchronizationListener listener) { if (listener != null) { listener.onSyncProgress(progress); } } private void parse(InputStream in, int startBundleNumber) throws XmlPullParserException, IOException { try { XmlPullParser parser = Xml.newPullParser(); parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); parser.setInput(in, null); parser.nextTag(); readImageBundles(parser, startBundleNumber); } finally { in.close(); } } private void readImageBundles(XmlPullParser parser, int startBundleNumber) throws XmlPullParserException, IOException { parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_ALL_BUNDLES_NAME); while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() == XmlPullParser.END_DOCUMENT) { throw new XmlPullParserException("Unexpected end of file."); } if (parser.getEventType() != XmlPullParser.START_TAG) { continue; } String name = parser.getName(); if (name.equals(TAG_BUNDLE_NAME)) { String bundleNumberRaw = parser.getAttributeValue(NAMESPACE, BUNDLE_ATTRIBUTE_VERSION_NAME); int bundleNumber; try { bundleNumber = Integer.parseInt(bundleNumberRaw); } catch (NumberFormatException nfe) { throw new XmlPullParserException("Bundle version number not a number: " + bundleNumberRaw); } String origin = parser.getAttributeValue(NAMESPACE, BUNDLE_ATTRIBUTE_ORIGIN); mCurrOrigin = origin; if (bundleNumber >= startBundleNumber) { List<Image> bundleImages = readBundle(parser); mReadBundles.put(bundleNumber, bundleImages); mReadBundlesOrigin.put(bundleNumber, origin); mHighestReadBundleNumber = Math.max(mHighestReadBundleNumber, bundleNumber); // so the bundles should be in ascending order in case of exceptions } else { skip(parser); } } else { skip(parser); } } } private List<Image> readBundle(XmlPullParser parser) throws XmlPullParserException, IOException { parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_BUNDLE_NAME); List<Image> bundleImages = new ArrayList<>(); while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() == XmlPullParser.END_DOCUMENT) { throw new XmlPullParserException("Unexpected end of file."); } if (parser.getEventType() != XmlPullParser.START_TAG) { continue; } String name = parser.getName(); if (name.equals(TAG_IMAGE_NAME)) { Image readImage = readImage(parser); if (readImage != null) { bundleImages.add(readImage); } } else { skip(parser); } } return bundleImages; } private Image readImage(XmlPullParser parser) throws XmlPullParserException, IOException { parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_IMAGE_NAME); Image.Builder builder = new Image.Builder(); builder.setOrigin(mCurrOrigin); while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() == XmlPullParser.END_DOCUMENT) { throw new XmlPullParserException("Unexpected end of file."); } if (parser.getEventType() != XmlPullParser.START_TAG) { continue; } String name = parser.getName(); switch (name) { case ImageTable.COLUMN_HASH: builder.setHash(readTextChecked(parser, ImageTable.COLUMN_HASH)); break; case ImageTable.COLUMN_RESNAME: builder.setResourceName(mContext, readTextChecked(parser, ImageTable.COLUMN_RESNAME)); break; case ImageTable.COLUMN_SOLUTIONS: builder.setSolutions(readSolutions(parser)); break; case ImageTable.COLUMN_AUTHOR: builder.setAuthor(readAuthor(parser)); break; case ImageTable.COLUMN_RIDDLEPREFTYPES: builder.setPreferredRiddleTypes(readPreferredRiddleTypes(parser)); break; case ImageTable.COLUMN_RIDDLEREFUSEDTYPES: builder.setRefusedRiddleTypes(readRefusedRiddleTypes(parser)); break; case ImageTable.COLUMN_ORIGIN: builder.setOrigin(readTextChecked(parser, ImageTable.COLUMN_ORIGIN)); break; case ImageTable.COLUMN_SAVELOC: builder.setRelativeImagePath(readTextChecked(parser, ImageTable.COLUMN_SAVELOC)); break; case ImageTable.COLUMN_OBFUSCATION: builder.setObfuscation(readTextChecked(parser, ImageTable.COLUMN_OBFUSCATION)); break; case ImageTable.COLUMN_AVERAGE_COLOR: builder.setAverageColor(readTextChecked(parser, ImageTable.COLUMN_AVERAGE_COLOR)); break; default: skip(parser); break; } } try { return builder.build(mContext, mBuffer); } catch (BuildException be) { if (mModeAbortOnImageBuildFailure) { throw new XmlPullParserException("Could not parse image: " + be); } Log.e("Image", "Failed parsing image, but not aborting: " + be); return null; // failure but do not abort } } private List<Solution> readSolutions(XmlPullParser parser) throws XmlPullParserException, IOException { parser.require(XmlPullParser.START_TAG, NAMESPACE, ImageTable.COLUMN_SOLUTIONS); List<Solution> solutions = new ArrayList<>(); while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() == XmlPullParser.END_DOCUMENT) { throw new XmlPullParserException("Unexpected end of file."); } if (parser.getEventType() != XmlPullParser.START_TAG) { continue; } String name = parser.getName(); if (name.equals(TAG_SOLUTION_NAME)) { solutions.add(readSolution(parser)); } else { skip(parser); } } return solutions; } private Solution readSolution(XmlPullParser parser) throws XmlPullParserException, IOException { parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_SOLUTION_NAME); Tongue tongue = null; List<String> solutionWords = new ArrayList<>(); while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() == XmlPullParser.END_DOCUMENT) { throw new XmlPullParserException("Unexpected end of file."); } if (parser.getEventType() != XmlPullParser.START_TAG) { continue; } if (parser.getName().equals(TAG_SOLUTION_TONGUE_NAME)) { tongue = Tongue.getByShortcut(readTextChecked(parser, TAG_SOLUTION_TONGUE_NAME)); } else if (parser.getName().equals(TAG_SOLUTION_WORD_NAME)) { String solWord = readTextChecked(parser, TAG_SOLUTION_WORD_NAME); if (!TextUtils.isEmpty(solWord)) { solutionWords.add(solWord); } } else { skip(parser); } } if (solutionWords.isEmpty() || tongue == null) { throw new XmlPullParserException("No tongue or solution words."); } return new Solution(tongue, solutionWords); } private ImageAuthor readAuthor(XmlPullParser parser) throws IOException, XmlPullParserException { parser.require(XmlPullParser.START_TAG, NAMESPACE, ImageTable.COLUMN_AUTHOR); String name = null; String source = null; String license = null; String title = null; String extras = null; while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() == XmlPullParser.END_DOCUMENT) { throw new XmlPullParserException("Unexpected end of file."); } if (parser.getEventType() != XmlPullParser.START_TAG) { continue; } if (parser.getName().equals(TAG_AUTHOR_NAME)) { name = readTextChecked(parser, TAG_AUTHOR_NAME); } else if (parser.getName().equals(TAG_AUTHOR_SOURCE)) { source = readTextChecked(parser, TAG_AUTHOR_SOURCE); } else if (parser.getName().equals(TAG_AUTHOR_LICENSE)) { license = readTextChecked(parser, TAG_AUTHOR_LICENSE); } else if (parser.getName().equals(TAG_AUTHOR_TITLE)) { title = readTextChecked(parser, TAG_AUTHOR_TITLE); } else if (parser.getName().equals(TAG_AUTHOR_EXTRAS)) { extras = readTextChecked(parser, TAG_AUTHOR_EXTRAS); } else { skip(parser); } } if (TextUtils.isEmpty(name)) { throw new XmlPullParserException("Missing ImageAuthor"); } return new ImageAuthor(name, source, license, title, extras); } private List<RiddleType> readPreferredRiddleTypes(XmlPullParser parser) throws IOException, XmlPullParserException { parser.require(XmlPullParser.START_TAG, NAMESPACE, ImageTable.COLUMN_RIDDLEPREFTYPES); List<RiddleType> types = new ArrayList<>(); while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() == XmlPullParser.END_DOCUMENT) { throw new XmlPullParserException("Unexpected end of file."); } if (parser.getEventType() != XmlPullParser.START_TAG) { continue; } if (parser.getName().equals(TAG_RIDDLE_TYPE_NAME)) { String data = readTextChecked(parser, TAG_RIDDLE_TYPE_NAME); if (!TextUtils.isEmpty(data)) { RiddleType type = RiddleType.getInstance(data); if (type != null) { types.add(type); } } } else { skip(parser); } } return types; } private List<RiddleType> readRefusedRiddleTypes(XmlPullParser parser) throws IOException, XmlPullParserException { parser.require(XmlPullParser.START_TAG, NAMESPACE, ImageTable.COLUMN_RIDDLEREFUSEDTYPES); List<RiddleType> types = new ArrayList<>(); while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() == XmlPullParser.END_DOCUMENT) { throw new XmlPullParserException("Unexpected end of file."); } if (parser.getEventType() != XmlPullParser.START_TAG) { continue; } if (parser.getName().equals(TAG_RIDDLE_TYPE_NAME)) { String data = readTextChecked(parser, TAG_RIDDLE_TYPE_NAME); if (!TextUtils.isEmpty(data)) { RiddleType type = RiddleType.getInstance(data); if (type != null) { types.add(type); } } } else { skip(parser); } } return types; } private String readTextChecked(XmlPullParser parser, String tag) throws IOException, XmlPullParserException { parser.require(XmlPullParser.START_TAG, NAMESPACE, tag); String text = readText(parser); parser.require(XmlPullParser.END_TAG, NAMESPACE, tag); return text; } private String readText(XmlPullParser parser) throws IOException, XmlPullParserException { String result = null; if (parser.next() == XmlPullParser.TEXT) { result = parser.getText(); parser.nextTag(); } return result; } private void skip(XmlPullParser parser) throws XmlPullParserException, IOException { if (parser.getEventType() != XmlPullParser.START_TAG) { throw new IllegalStateException(); } int depth = 1; while (depth != 0) { switch (parser.next()) { case XmlPullParser.END_TAG: depth--; break; case XmlPullParser.START_TAG: depth++; break; } } } public int getReadBundlesCount() { return mReadBundles == null ? 0 : mReadBundles.size(); } public int getHighestReadBundleNumber() { return mHighestReadBundleNumber; } public String getOrigin(Integer bundleNumber) { return mReadBundlesOrigin.get(bundleNumber); } }