/*
GNU General Public License
CacheWolf is a software for PocketPC, Win and Linux that
enables paperless caching.
It supports the sites geocaching.com and opencaching.de
Copyright (C) 2006 CacheWolf development team
See http://www.cachewolf.de/ for more information.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package CacheWolf.database;
import com.stevesoft.ewe_pat.Regex;
import CacheWolf.MainForm;
import CacheWolf.Preferences;
import CacheWolf.Profile;
import CacheWolf.controls.InfoBox;
import CacheWolf.utils.Extractor;
import CacheWolf.utils.MyLocale;
import CacheWolf.utils.STRreplace;
import CacheWolf.utils.SafeXML;
import ewe.io.BufferedWriter;
import ewe.io.File;
import ewe.io.FileReader;
import ewe.io.FileWriter;
import ewe.io.PrintWriter;
import ewe.ui.FormBase;
import ewe.util.mString;
public class CacheHolderDetail {
/** CacheHolder of the detail. <b>Only</b> set by CacheHolder when creating detail! **/
private CacheHolder parent = null;
private String version = "";
public String LongDescription = "";
public String LastUpdate = "";
public String Hints = "";
public LogList CacheLogs = new LogList();
private String CacheNotes = "";
public CacheImages images = new CacheImages();
public CacheImages logImages = new CacheImages();
public CacheImages userImages = new CacheImages();
public Attributes attributes = new Attributes();
public TravelbugList Travelbugs = new TravelbugList();
// public String Bugs = ""; Superceded by Travelbugs
public String URL = "";
private String solver;
private Log ownLog;
private String country;
private String state;
/**
* For faster cache import (from opencaching) changes are only written when the details are freed from memory
* If you want to save the changes automatically when the details are unloaded, set this to true
*/
public boolean hasUnsavedChanges = false;
public CacheHolderDetail(CacheHolder ch) {
parent = ch;
solver = "";
ownLog = null;
country = "";
state = "";
}
// quick debug info
public String toString() {
if (this.parent == null)
return "empty unassigned";
else if (parent.mainCache == null)
return parent.toString();
else
return parent + "(" + parent.mainCache + ")";
}
public CacheHolder getParent() {
return parent;
}
public String getSolver() {
return this.solver;
}
public void setSolver(String solver) {
if (!this.solver.equals(solver))
parent.setUpdated(true);
parent.setHasSolver(!solver.trim().equals(""));
this.solver = solver;
}
public Log getOwnLog() {
return this.ownLog;
}
public void setOwnLog(Log ownLog) {
this.ownLog = ownLog;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public void setLongDescription(String longDescription) {
String s = stripControlChars(longDescription);
if (LongDescription.equals(""))
parent.setNew(true);
else {
if (!s.equals(LongDescription)) {
parent.setUpdated(true);
}
}
LongDescription = s;
}
private String stripControlChars(String desc) {
StringBuffer sb = new StringBuffer(desc.length());
for (int i = 0; i < desc.length(); i++) {
char c = desc.charAt(i);
if (c >= ' ' || c == 10 || c == 13)
sb.append(c);
}
return sb.toString();
}
public void setHints(String hints) {
if (!Hints.equals(hints))
parent.setUpdated(true);
Hints = hints;
}
private String gCNotes = "";
public String getGCNotes() {
return gCNotes;
}
public void setGCNotes(String notes) {
gCNotes = notes;
}
public boolean setCacheNotes(String notes) {
boolean ret = !this.CacheNotes.equals(notes);
if (ret) {
parent.setUpdated(true);
this.CacheNotes = notes;
parent.setHasNote(!this.CacheNotes.trim().equals(""));
}
return ret;
}
public String getCacheNotes() {
return this.CacheNotes;
}
public void setCacheLogs(LogList newLogs) {
if (Preferences.itself().overwriteLogs) {
CacheLogs = newLogs;
parent.setLogUpdated(true);
hasUnsavedChanges = true;
} else {
int size = newLogs.size();
for (int i = size - 1; i >= 0; i--) { // Loop over all new logs, must start with oldest log
if (CacheLogs.merge(newLogs.getLog(i)) >= 0)
parent.setLogUpdated(true);
}
}
if (CacheLogs.purgeLogs() > 0)
hasUnsavedChanges = true;
parent.setNoFindLogs(CacheLogs.countNotFoundLogs());
}
/**
* Method to update an existing cache with new data. This is
* necessary to avoid missing old logs. Called from GPX Importer
*
* @param newChD
* new cache data
* @return CacheHolder with updated data
*/
public CacheHolderDetail update(CacheHolderDetail newChD) {
// flags
CacheHolder ch = parent;
// travelbugs:GPX-File contains all actual travelbugs but not the missions
// we need to check whether the travelbug is already in the existing list
ch.hasBugs(newChD.Travelbugs.size() > 0);
for (int i = newChD.Travelbugs.size() - 1; i >= 0; i--) {
Travelbug tb = newChD.Travelbugs.getTB(i);
Travelbug oldTB = this.Travelbugs.find(tb.getName());
// If the bug is already in the cache, we keep it
if (oldTB != null) {
if (tb.getMission().length() > 0)
oldTB.setMission(tb.getMission());
if (tb.getGuid().length() > 0)
oldTB.setGuid(tb.getGuid());
newChD.Travelbugs.replace(i, oldTB);
}
}
this.Travelbugs = newChD.Travelbugs;
if (newChD.attributes.count() > 0)
this.attributes = newChD.attributes;
this.URL = newChD.URL;
if (this.gCNotes.length() > 0) {
this.setCacheNotes(STRreplace.replace(this.CacheNotes, this.gCNotes, newChD.getGCNotes()));
} else {
this.setCacheNotes(this.CacheNotes + newChD.getGCNotes());
}
this.images = newChD.images;
setLongDescription(newChD.LongDescription);
setHints(newChD.Hints);
setCacheLogs(newChD.CacheLogs);
if (newChD.ownLog != null) {
this.ownLog = newChD.ownLog;
}
if (newChD.country.length() > 0)
this.country = newChD.country;
if (newChD.state.length() > 0)
this.state = newChD.state;
if (newChD.getSolver().length() > 0)
this.setSolver(newChD.getSolver());
return this;
}
/**
* Method to parse a specific cache.xml file.<br>
* It fills information on cache details, hints, logs, notes and images.<br>
*
* @param dir
*/
void readCache(String dir) {
FileReader in = null;
CacheImage imageInfo;
// If parent cache has empty waypoint then don't do anything.<br>
// This might happen when a cache object is freshly created to serve as container for imported data
if (this.parent.getCode().length() == 0)
return;
File cacheFile = new File(dir + parent.getCode().toLowerCase() + ".xml"); // Kleinschreibung
if (cacheFile.exists()) {
try {
in = new FileReader(cacheFile.getAbsolutePath());
} catch (Exception e) {
in = null;
}
}
if (in == null) {
cacheFile = new File(dir + parent.getCode() + ".xml"); // gespeicherte Schreibweise
if (cacheFile.exists()) {
try {
in = new FileReader(cacheFile.getAbsolutePath());
} catch (Exception e) {
}
} else {
return; // leerer neuer Wegpunkt
}
}
String text = "";
try {
text = in.readAll();
} catch (Exception e) {
}
try {
in.close();
} catch (Exception e) {
}
if (text.length() == 0) {
new InfoBox(MyLocale.getMsg(5500, "Error"), MyLocale.getMsg(31415, "Could not read cache details for cache: ") + parent.getCode()).wait(FormBase.OKB);
return;
}
Extractor ex = new Extractor(text, "<VERSION value = \"", "\"/>", 0, true);
version = ex.findNext();
LongDescription = ex.findNext("<DETAILS><![CDATA[", "]]></DETAILS>");
country = ex.findNext("<COUNTRY><![CDATA[", "]]></COUNTRY>");
state = ex.findNext("<STATE><![CDATA[", "]]></STATE>");
attributes.XmlAttributesEnd(ex.findNext("<ATTRIBUTES>", "</ATTRIBUTES>"));
Hints = ex.findNext("<HINTS><![CDATA[", "]]></HINTS>");
Extractor subex = new Extractor(ex.findNext("<LOGS>", "</LOGS>"), "<OWNLOGID>", "</OWNLOGID>", 0, true);
String OwnLogId = subex.findNext();
String ownLogText = subex.findNext("<OWNLOG><![CDATA[", "]]></OWNLOG>");
if (ownLogText.length() > 0) {
if (ownLogText.indexOf("<img src='") >= 0) {
ownLog = new Log(ownLogText + "]]>");
ownLog.setLogID(OwnLogId);
ownLog.setFinderID(Preferences.itself().gcMemberId);
} else {
ownLog = new Log(OwnLogId, Preferences.itself().gcMemberId, "2.png", "1900-02-02", Preferences.itself().myAlias, ownLogText);
}
} else {
ownLog = null;
}
CacheLogs.clear();
String dummy = subex.findNext("<LOG>", "</LOG>");
while (dummy.length() > 0) {
CacheLogs.add(new Log(dummy));
dummy = subex.findNext();
}
CacheNotes = ex.findNext("<NOTES><![CDATA[", "]]></NOTES>");
gCNotes = new Extractor(CacheNotes, "<GC>", "</GC>", 0, false).findNext();
if (version.equals("3")) {
images.clear();
int searchStart = 0;
subex.set(ex.findNext("<IMAGES>", "</IMAGES"), "<IMG>", "</IMG>", 0, true);
while ((dummy = subex.findNext()).length() > 0) {
int pos = dummy.indexOf("<URL>");
imageInfo = new CacheImage(CacheImage.FROMUNKNOWN);
if (pos > 0) {
imageInfo.setFilename(SafeXML.html2iso8859s1(dummy.substring(0, pos)));
imageInfo.setURL(SafeXML.html2iso8859s1((dummy.substring(pos + 5, dummy.indexOf("</URL>")))));
} else {
imageInfo.setFilename(SafeXML.html2iso8859s1(dummy));
}
this.images.add(imageInfo);
searchStart = subex.searchedFrom();
}
subex.set("<IMGTEXT>", "</IMGTEXT>", searchStart);
int imgNr = 0;
while ((dummy = subex.findNext()).length() > 0) {
if (imgNr >= this.images.size()) {
// this (more IMGTEXT than IMG in the <cache>.xml, but it happens. So avoid an ArrayIndexOutOfBoundException and add an CacheImage gracefully
images.add(new CacheImage(CacheImage.FROMUNKNOWN));
Preferences.itself().log("Error reading " + this.parent.getCode() + "More IMGTEXT tags than IMG tags");
}
imageInfo = this.images.get(imgNr);
int pos = dummy.indexOf("<DESC>");
if (pos > 0) {
imageInfo.setTitle(dummy.substring(0, pos));
imageInfo.setComment(dummy.substring(pos + 6, dummy.indexOf("</DESC>")));
} else {
imageInfo.setTitle(dummy);
}
imgNr = imgNr + 1;
searchStart = subex.searchedFrom();
}
logImages.clear();
subex.set("<LOGIMG>", "</LOGIMG>", searchStart);
while ((dummy = subex.findNext()).length() > 0) {
imageInfo = new CacheImage(CacheImage.FROMLOG);
imageInfo.setFilename(dummy);
logImages.add(imageInfo);
searchStart = subex.searchedFrom();
}
subex.set("<LOGIMGTEXT>", "</LOGIMGTEXT>", searchStart);
imgNr = 0;
while ((dummy = subex.findNext()).length() > 0) {
imageInfo = logImages.get(imgNr++);
imageInfo.setTitle(dummy);
searchStart = subex.searchedFrom();
}
userImages.clear();
subex.set("<USERIMG>", "</USERIMG>", searchStart);
while ((dummy = subex.findNext()).length() > 0) {
imageInfo = new CacheImage(CacheImage.FROMUSER);
imageInfo.setFilename(dummy);
userImages.add(imageInfo);
searchStart = subex.searchedFrom();
}
subex.set("<USERIMGTEXT>", "</USERIMGTEXT>", searchStart);
imgNr = 0;
while ((dummy = subex.findNext()).length() > 0) {
imageInfo = userImages.get(imgNr++);
imageInfo.setTitle(dummy);
searchStart = subex.searchedFrom();
}
} else if (version.equals("4")) {
String tmp = ex.findNext("<IMAGES>", "</IMAGES");
subex.set(tmp, "<IMG ", "</IMG>", 0, true);
this.images.clear();
this.logImages.clear();
this.userImages.clear();
Regex getImageInfos = new Regex("SRC=\"(.*?)\"( URL=\"(.*?)\")?( TITLE=\"(.*?)\")?( CMT=\"(.*?)\")?");
while ((dummy = subex.findNext()).length() > 0) {
String[] parts = mString.split(dummy, '>');
getImageInfos.search(parts[0]);
// if (parts[0].startsWith("SRC=\"4\"")) {
String ssrc = getImageInfos.stringMatched(1);
if (ssrc != null) {
char src = ssrc.charAt(0);
imageInfo = new CacheImage(src);
imageInfo.setFilename(SafeXML.html2iso8859s1(parts[1]));
imageInfo.setURL(SafeXML.html2iso8859s1(getImageInfos.stringMatched(3)));
imageInfo.setTitle(getImageInfos.stringMatched(5));
imageInfo.setComment(getImageInfos.stringMatched(7));
switch (src) {
case CacheImage.FROMDESCRIPTION:
this.images.add(imageInfo);
break;
case CacheImage.FROMLOG:
this.logImages.add(imageInfo);
break;
case CacheImage.FROMSPOILER:
this.images.add(imageInfo);
break;
case CacheImage.FROMUSER:
this.userImages.add(imageInfo);
break;
default:
// ist wohl 0 (als update Relikt von version 3)
this.images.add(imageInfo);
continue;
}
}
}
}
dummy = ex.findNext("<TRAVELBUGS>", "</TRAVELBUGS>");
if (dummy.length() > 10) {
Travelbugs.addFromXML(dummy);
}
dummy = ex.findNext("<URL><![CDATA[", "]]></URL>");
if (dummy.length() > 10) {
URL = dummy;
int logpos = URL.indexOf("&"); // &Submit &log=y
if (logpos > 0)
URL = URL.substring(0, logpos);
} else {
// if no URL is stored, set default URL (at this time only possible for gc.com)
if (parent.isGC()) {
URL = "http://www.geocaching.com/seek/cache_details.aspx?wp=" + parent.getCode();
}
}
this.setSolver(ex.findNext("<SOLVER><![CDATA[", "]]></SOLVER>"));
}
public void deleteFile(String FileName) {
// File exists?
boolean exists = (new File(FileName)).exists();
// yes: then delete
if (exists) {
boolean ok = (new File(FileName)).delete();
if (ok)
ok = true;
}
boolean exists2 = (new File(FileName.toLowerCase())).exists();
// yes: delete
if (exists2) {
boolean ok2 = (new File(FileName.toLowerCase())).delete();
if (ok2)
ok2 = true;
}
}
PrintWriter detfile;
/**
* Method to save a cache.xml file.
*/
public void saveCacheDetails(String dir) {
deleteFile(dir + parent.getCode() + ".xml");
try {
detfile = new PrintWriter(new BufferedWriter(new FileWriter(new File(dir + parent.getCode().toLowerCase() + ".xml").getAbsolutePath())));
} catch (Exception e) {
Preferences.itself().log("Problem creating details file", e, true);
return;
}
try {
if (parent.getCode().length() > 0) {
detfile.print("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n");
detfile.print("<CACHEDETAILS>\r\n");
detfile.print("<VERSION value = \"" + Profile.CURRENTFILEFORMAT + "\"/>\n");
detfile.print("<DETAILS><![CDATA[" + LongDescription + "]]></DETAILS>\r\n");
detfile.print("<COUNTRY><![CDATA[" + country + "]]></COUNTRY>\n");
detfile.print("<STATE><![CDATA[" + state + "]]></STATE>\n");
detfile.print(attributes.XmlAttributesWrite());
detfile.print("<HINTS><![CDATA[" + Hints + "]]></HINTS>\r\n");
detfile.print("<LOGS>\r\n");
if (ownLog != null) {
detfile.print("<OWNLOGID>" + ownLog.getLogID() + "</OWNLOGID>\r\n");
detfile.print("<OWNLOG><![CDATA[" + ownLog.toHtml() + "]]></OWNLOG>\r\n");
} else {
detfile.print("<OWNLOGID></OWNLOGID>\r\n");
detfile.print("<OWNLOG><![CDATA[]]></OWNLOG>\r\n");
}
for (int i = 0; i < CacheLogs.size(); i++) {
detfile.print(CacheLogs.getLog(i).toXML());
}
detfile.print("</LOGS>\r\n");
detfile.print("<NOTES><![CDATA[" + CacheNotes + "]]></NOTES>\n");
detfile.print("<IMAGES>\n");
printImages(images);
printImages(logImages);
printImages(userImages);
detfile.print("</IMAGES>\n");
detfile.print(Travelbugs.toXML());
detfile.print("<URL><![CDATA[" + URL + "]]></URL>\r\n");
detfile.print("<SOLVER><![CDATA[" + getSolver() + "]]></SOLVER>\r\n");
detfile.print(parent.toXML()); // This will allow restoration of index.xml
detfile.print("</CACHEDETAILS>\n");
// Preferences.itself().log("Writing file: " + parent.getCode().toLowerCase() + ".xml");
} // if length
} catch (Exception e) {
Preferences.itself().log("Problem waypoint " + parent.getCode() + " writing to a details file: ", e);
}
try {
detfile.close();
} catch (Exception e) {
Preferences.itself().log("Problem waypoint " + parent.getCode() + " writing to a details file: ", e);
}
hasUnsavedChanges = false;
}
private void printImages(CacheImages imgs) {
for (int i = 0; i < imgs.size(); i++) {
CacheImage img = imgs.get(i);
detfile.print("<IMG");
detfile.print(addAttribute("SRC", String.valueOf(img.getSource())));
detfile.print(addAttribute("URL", SafeXML.string2Html(img.getURL())));
detfile.print(addAttribute("TITLE", img.getTitle()));
detfile.print(addAttribute("CMT", img.getComment()));
detfile.print(">");
detfile.print(SafeXML.string2Html(img.getFilename()));
detfile.print("</IMG>\n");
}
}
private String addAttribute(String att, String attValue) {
if (attValue.length() > 0) {
return " " + att + "=\"" + attValue + "\"";
} else {
return "";
}
}
/**
* Return true if this cache has additional info for some pictures
*
* @return true if cache has additional info, false otherwise
*/
public boolean hasCacheImage() {
for (int i = this.images.size() - 1; i >= 0; i--)
if (!this.images.get(i).getComment().equals(""))
return true;
return false;
}
/**
* change id in waypoint details and rename associated files. Function should only be called by CacheHolder
*
* @param newWptId
* new id of the waypoint
* @return true on success, false for failure
*/
protected boolean rename(String newWptId) {
boolean success = false;
String profiledir = MainForm.profile.dataDir;
int oldWptLength = parent.getCode().length();
// just in case ... (got the pun? ;) )
newWptId = newWptId.toUpperCase();
// update image information
for (int i = 0; i < images.size(); i++) {
String filename = images.get(i).getFilename();
String comment = images.get(i).getComment();
String title = images.get(i).getTitle();
if (filename.indexOf(parent.getCode()) == 0) {
filename = newWptId.concat(filename.substring(oldWptLength));
images.get(i).setFilename(filename);
}
if (comment.indexOf(parent.getCode()) == 0) {
comment = newWptId.concat(comment.substring(oldWptLength));
images.get(i).setComment(comment);
}
if (title.indexOf(parent.getCode()) == 0) {
title = newWptId.concat(title.substring(oldWptLength));
images.get(i).setTitle(title);
}
}
for (int i = 0; i < logImages.size(); i++) {
String filename = logImages.get(i).getFilename();
String comment = logImages.get(i).getComment();
String title = logImages.get(i).getTitle();
if (filename.indexOf(parent.getCode()) == 0) {
filename = newWptId.concat(filename.substring(oldWptLength));
logImages.get(i).setFilename(filename);
}
if (comment.indexOf(parent.getCode()) == 0) {
comment = newWptId.concat(comment.substring(oldWptLength));
logImages.get(i).setComment(comment);
}
if (title.indexOf(parent.getCode()) == 0) {
title = newWptId.concat(title.substring(oldWptLength));
logImages.get(i).setTitle(title);
}
}
for (int i = 0; i < userImages.size(); i++) {
String filename = userImages.get(i).getFilename();
String comment = userImages.get(i).getComment();
String title = userImages.get(i).getTitle();
if (filename.indexOf(parent.getCode()) == 0) {
filename = newWptId.concat(filename.substring(oldWptLength));
userImages.get(i).setFilename(filename);
}
if (comment.indexOf(parent.getCode()) == 0) {
comment = newWptId.concat(comment.substring(oldWptLength));
userImages.get(i).setComment(comment);
}
if (title.indexOf(parent.getCode()) == 0) {
title = newWptId.concat(title.substring(oldWptLength));
userImages.get(i).setTitle(title);
}
}
// rename the files
try {
// since we use *.* we do not need FileBugFix
String srcFiles[] = new File(profiledir).list(parent.getCode().concat("*.*"), ewe.io.FileBase.LIST_FILES_ONLY);
for (int i = 0; i < srcFiles.length; i++) {
String newfile = newWptId.concat(srcFiles[i].substring(oldWptLength));
File srcFile = new File(profiledir.concat(srcFiles[i]));
File dstFile = new File(profiledir.concat(newfile));
srcFile.move(dstFile);
}
success = true;
} catch (Exception e) {
Preferences.itself().log("Error renaming waypoint details", e, true);
// TODO: any chance of a roll back?
// TODO: should we ignore a file not found?
}
hasUnsavedChanges = true;
return success;
}
}