mirror of
https://github.com/jenkinsci/jenkins.git
synced 2026-11-11 11:06:25 +00:00
git-svn-id: https://hudson.dev.java.net/svn/hudson/trunk/hudson/main@15081 71c3de6d-444a-0410-be80-ed276b4c234a
286 lines
9.6 KiB
Java
286 lines
9.6 KiB
Java
/*
|
|
* The MIT License
|
|
*
|
|
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*/
|
|
package hudson;
|
|
|
|
import com.thoughtworks.xstream.XStream;
|
|
import com.thoughtworks.xstream.converters.ConversionException;
|
|
import com.thoughtworks.xstream.converters.Converter;
|
|
import com.thoughtworks.xstream.io.StreamException;
|
|
import com.thoughtworks.xstream.io.xml.XppReader;
|
|
import hudson.model.Descriptor;
|
|
import hudson.util.AtomicFileWriter;
|
|
import hudson.util.IOException2;
|
|
import hudson.util.XStream2;
|
|
import org.xml.sax.Attributes;
|
|
import org.xml.sax.Locator;
|
|
import org.xml.sax.SAXException;
|
|
import org.xml.sax.ext.Locator2;
|
|
import org.xml.sax.helpers.DefaultHandler;
|
|
|
|
import javax.xml.parsers.ParserConfigurationException;
|
|
import javax.xml.parsers.SAXParserFactory;
|
|
import java.io.BufferedReader;
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStreamReader;
|
|
import java.io.Reader;
|
|
import java.io.Writer;
|
|
import java.io.StringWriter;
|
|
import java.util.logging.Logger;
|
|
|
|
/**
|
|
* Represents an XML data file that Hudson uses as a data file.
|
|
*
|
|
*
|
|
* <h2>Evolving data format</h2>
|
|
* <p>
|
|
* Changing data format requires a particular care so that users with
|
|
* the old data format can migrate to the newer data format smoothly.
|
|
*
|
|
* <p>
|
|
* Adding a field is the easiest. When you read an old XML that does
|
|
* not have any data, the newly added field is left to the VM-default
|
|
* value (if you let XStream create the object, such as
|
|
* {@link #read()} — which is the majority), or to the value initialized by the
|
|
* constructor (if the object is created via <tt>new</tt> and then its
|
|
* value filled by XStream, such as {@link #unmarshal(Object)}.)
|
|
*
|
|
* <p>
|
|
* Removing a field requires that you actually leave the field with
|
|
* <tt>transient</tt> keyword. When you read the old XML, XStream
|
|
* will set the value to this field. But when the data is saved,
|
|
* the field will no longer will be written back to XML.
|
|
* (It might be possible to tweak XStream so that we can simply
|
|
* remove fields from the class. Any help appreciated.)
|
|
*
|
|
* <p>
|
|
* Changing the data structure is usually a combination of the two
|
|
* above. You'd leave the old data store with <tt>transient</tt>,
|
|
* and then add the new data. When you are reading the old XML,
|
|
* only the old field will be set. When you are reading the new XML,
|
|
* only the new field will be set. You'll then need to alter the code
|
|
* so that it will be able to correctly handle both situations,
|
|
* and that as soon as you see data in the old field, you'll have to convert
|
|
* that into the new data structure, so that the next <tt>save</tt> operation
|
|
* will write the new data (otherwise you'll end up losing the data, because
|
|
* old fields will be never written back.)
|
|
*
|
|
* <p>
|
|
* In some limited cases (specifically when the class is the root object
|
|
* to be read from XML, such as {@link Descriptor}), it is possible
|
|
* to completely and drastically change the data format. See
|
|
* {@link Descriptor#load()} for more about this technique.
|
|
*
|
|
* <p>
|
|
* There's a few other possibilities, such as implementing a custom
|
|
* {@link Converter} for XStream, or {@link XStream#alias(String, Class) registering an alias}.
|
|
*
|
|
* @author Kohsuke Kawaguchi
|
|
*/
|
|
public final class XmlFile {
|
|
private final XStream xs;
|
|
private final File file;
|
|
|
|
public XmlFile(File file) {
|
|
this(DEFAULT_XSTREAM,file);
|
|
}
|
|
|
|
public XmlFile(XStream xs, File file) {
|
|
this.xs = xs;
|
|
this.file = file;
|
|
}
|
|
|
|
public File getFile() {
|
|
return file;
|
|
}
|
|
|
|
/**
|
|
* Loads the contents of this file into a new object.
|
|
*/
|
|
public Object read() throws IOException {
|
|
LOGGER.fine("Reading "+file);
|
|
Reader r = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
|
|
try {
|
|
return xs.fromXML(r);
|
|
} catch(StreamException e) {
|
|
throw new IOException2("Unable to read "+file,e);
|
|
} catch(ConversionException e) {
|
|
throw new IOException2("Unable to read "+file,e);
|
|
} finally {
|
|
r.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads the contents of this file into an existing object.
|
|
*
|
|
* @return
|
|
* The unmarshalled object. Usually the same as <tt>o</tt>, but would be different
|
|
* if the XML representation if completely new.
|
|
*/
|
|
public Object unmarshal( Object o ) throws IOException {
|
|
Reader r = new BufferedReader(new InputStreamReader(new FileInputStream(file),"UTF-8"));
|
|
try {
|
|
return xs.unmarshal(new XppReader(r),o);
|
|
} catch (StreamException e) {
|
|
throw new IOException2(e);
|
|
} catch(ConversionException e) {
|
|
throw new IOException2("Unable to read "+file,e);
|
|
} finally {
|
|
r.close();
|
|
}
|
|
}
|
|
|
|
public void write( Object o ) throws IOException {
|
|
mkdirs();
|
|
AtomicFileWriter w = new AtomicFileWriter(file);
|
|
try {
|
|
w.write("<?xml version='1.0' encoding='UTF-8'?>\n");
|
|
xs.toXML(o,w);
|
|
w.commit();
|
|
} catch(StreamException e) {
|
|
throw new IOException2(e);
|
|
} finally {
|
|
w.close();
|
|
}
|
|
}
|
|
|
|
public boolean exists() {
|
|
return file.exists();
|
|
}
|
|
|
|
public void delete() {
|
|
file.delete();
|
|
}
|
|
|
|
public void mkdirs() {
|
|
file.getParentFile().mkdirs();
|
|
}
|
|
|
|
public String toString() {
|
|
return file.toString();
|
|
}
|
|
|
|
/**
|
|
* Opens a {@link Reader} that loads XML.
|
|
* This method uses {@link #sniffEncoding() the right encoding},
|
|
* not just the system default encoding.
|
|
*/
|
|
public Reader readRaw() throws IOException {
|
|
return new InputStreamReader(new FileInputStream(file),sniffEncoding());
|
|
}
|
|
|
|
/**
|
|
* Returns the XML file read as a string.
|
|
*/
|
|
public String asString() throws IOException {
|
|
StringWriter w = new StringWriter();
|
|
writeRawTo(w);
|
|
return w.toString();
|
|
}
|
|
|
|
/**
|
|
* Writes the raw XML to the given {@link Writer}.
|
|
* Writer will not be closed by the implementation.
|
|
*/
|
|
public void writeRawTo(Writer w) throws IOException {
|
|
Reader r = readRaw();
|
|
try {
|
|
Util.copyStream(r,w);
|
|
} finally {
|
|
r.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses the beginning of the file and determines the encoding.
|
|
*
|
|
* @throws IOException
|
|
* if failed to detect encoding.
|
|
* @return
|
|
* always non-null.
|
|
*/
|
|
public String sniffEncoding() throws IOException {
|
|
class Eureka extends SAXException {
|
|
final String encoding;
|
|
public Eureka(String encoding) {
|
|
this.encoding = encoding;
|
|
}
|
|
}
|
|
try {
|
|
JAXP.newSAXParser().parse(file,new DefaultHandler() {
|
|
private Locator loc;
|
|
public void setDocumentLocator(Locator locator) {
|
|
this.loc = locator;
|
|
}
|
|
|
|
public void startDocument() throws SAXException {
|
|
attempt();
|
|
}
|
|
|
|
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
|
|
attempt();
|
|
// if we still haven't found it at the first start element,
|
|
// there's something wrong.
|
|
throw new Eureka(null);
|
|
}
|
|
|
|
private void attempt() throws Eureka {
|
|
if(loc==null) return;
|
|
if (loc instanceof Locator2) {
|
|
Locator2 loc2 = (Locator2) loc;
|
|
String e = loc2.getEncoding();
|
|
if(e!=null)
|
|
throw new Eureka(e);
|
|
}
|
|
}
|
|
});
|
|
// can't reach here
|
|
throw new AssertionError();
|
|
} catch (Eureka e) {
|
|
if(e.encoding==null)
|
|
throw new IOException("Failed to detect encoding of "+file);
|
|
return e.encoding;
|
|
} catch (SAXException e) {
|
|
throw new IOException2("Failed to detect encoding of "+file,e);
|
|
} catch (ParserConfigurationException e) {
|
|
throw new AssertionError(e); // impossible
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@link XStream} instance is supposed to be thread-safe.
|
|
*/
|
|
private static final XStream DEFAULT_XSTREAM = new XStream2();
|
|
|
|
private static final Logger LOGGER = Logger.getLogger(XmlFile.class.getName());
|
|
|
|
private static final SAXParserFactory JAXP = SAXParserFactory.newInstance();
|
|
|
|
static {
|
|
JAXP.setNamespaceAware(true);
|
|
}
|
|
}
|