001: /*
002: * Copyright 2002-2007 the original author or authors.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.springframework.orm.jpa.persistenceunit;
018:
019: import java.io.IOException;
020: import java.io.InputStream;
021: import java.net.URL;
022: import java.util.LinkedList;
023: import java.util.List;
024:
025: import javax.persistence.spi.PersistenceUnitTransactionType;
026: import javax.xml.XMLConstants;
027: import javax.xml.parsers.DocumentBuilder;
028: import javax.xml.parsers.DocumentBuilderFactory;
029: import javax.xml.parsers.ParserConfigurationException;
030:
031: import org.apache.commons.logging.Log;
032: import org.apache.commons.logging.LogFactory;
033: import org.w3c.dom.Document;
034: import org.w3c.dom.Element;
035: import org.xml.sax.ErrorHandler;
036: import org.xml.sax.SAXException;
037:
038: import org.springframework.core.io.Resource;
039: import org.springframework.core.io.support.ResourcePatternResolver;
040: import org.springframework.jdbc.datasource.lookup.DataSourceLookup;
041: import org.springframework.util.Assert;
042: import org.springframework.util.ResourceUtils;
043: import org.springframework.util.StringUtils;
044: import org.springframework.util.xml.DomUtils;
045: import org.springframework.util.xml.SimpleSaxErrorHandler;
046:
047: /**
048: * Internal helper class for reading <code>persistence.xml</code> files.
049: *
050: * @author Costin Leau
051: * @author Juergen Hoeller
052: * @since 2.0
053: */
054: class PersistenceUnitReader {
055:
056: private static final String MAPPING_FILE_NAME = "mapping-file";
057:
058: private static final String JAR_FILE_URL = "jar-file";
059:
060: private static final String MANAGED_CLASS_NAME = "class";
061:
062: private static final String PROPERTIES = "properties";
063:
064: private static final String PROVIDER = "provider";
065:
066: private static final String EXCLUDE_UNLISTED_CLASSES = "exclude-unlisted-classes";
067:
068: private static final String NON_JTA_DATA_SOURCE = "non-jta-data-source";
069:
070: private static final String JTA_DATA_SOURCE = "jta-data-source";
071:
072: private static final String TRANSACTION_TYPE = "transaction-type";
073:
074: private static final String PERSISTENCE_UNIT = "persistence-unit";
075:
076: private static final String UNIT_NAME = "name";
077:
078: private static final String META_INF = "META-INF";
079:
080: private static final String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
081:
082: private static final String JAXP_SCHEMA_SOURCE = "http://java.sun.com/xml/jaxp/properties/schemaSource";
083:
084: private static final String SCHEMA_NAME = "persistence_1_0.xsd";
085:
086: private static final String[] SCHEMA_RESOURCE_LOCATIONS = {
087: "classpath:persistence_1_0.xsd",
088: "classpath:org/hibernate/ejb/persistence_1_0.xsd",
089: "classpath:org/jpox/jpa/persistence_1_0.xsd" };
090:
091: private final Log logger = LogFactory.getLog(getClass());
092:
093: private final ResourcePatternResolver resourcePatternResolver;
094:
095: private final DataSourceLookup dataSourceLookup;
096:
097: /**
098: * Create a new PersistenceUnitReader.
099: * @param resourcePatternResolver the ResourcePatternResolver to use for loading resources
100: * @param dataSourceLookup the DataSourceLookup to resolve DataSource names in
101: * <code>persistence.xml</code> files against
102: */
103: public PersistenceUnitReader(
104: ResourcePatternResolver resourcePatternResolver,
105: DataSourceLookup dataSourceLookup) {
106: Assert.notNull(resourcePatternResolver,
107: "ResourceLoader must not be null");
108: Assert.notNull(dataSourceLookup,
109: "DataSourceLookup must not be null");
110: this .resourcePatternResolver = resourcePatternResolver;
111: this .dataSourceLookup = dataSourceLookup;
112: }
113:
114: /**
115: * Parse and build all persistence unit infos defined in the specified XML file(s).
116: * @param persistenceXmlLocation the resource location (can be a pattern)
117: * @return the resulting PersistenceUnitInfo instances
118: */
119: public SpringPersistenceUnitInfo[] readPersistenceUnitInfos(
120: String persistenceXmlLocation) {
121: return readPersistenceUnitInfos(new String[] { persistenceXmlLocation });
122: }
123:
124: /**
125: * Parse and build all persistence unit infos defined in the given XML files.
126: * @param persistenceXmlLocations the resource locations (can be patterns)
127: * @return the resulting PersistenceUnitInfo instances
128: */
129: public SpringPersistenceUnitInfo[] readPersistenceUnitInfos(
130: String[] persistenceXmlLocations) {
131: ErrorHandler handler = new SimpleSaxErrorHandler(logger);
132: List<SpringPersistenceUnitInfo> infos = new LinkedList<SpringPersistenceUnitInfo>();
133: String resourceLocation = null;
134: try {
135: for (int i = 0; i < persistenceXmlLocations.length; i++) {
136: Resource[] resources = this .resourcePatternResolver
137: .getResources(persistenceXmlLocations[i]);
138: for (Resource resource : resources) {
139: resourceLocation = resource.toString();
140: InputStream stream = resource.getInputStream();
141: try {
142: Document document = validateResource(handler,
143: stream);
144: parseDocument(resource, document, infos);
145: } finally {
146: stream.close();
147: }
148: }
149: }
150: } catch (IOException ex) {
151: throw new IllegalArgumentException(
152: "Cannot parse persistence unit from "
153: + resourceLocation, ex);
154: } catch (SAXException ex) {
155: throw new IllegalArgumentException(
156: "Invalid XML in persistence unit from "
157: + resourceLocation, ex);
158: } catch (ParserConfigurationException ex) {
159: throw new IllegalArgumentException(
160: "Internal error parsing persistence unit from "
161: + resourceLocation);
162: }
163:
164: return infos
165: .toArray(new SpringPersistenceUnitInfo[infos.size()]);
166: }
167:
168: /**
169: * Validate the given stream and return a valid DOM document for parsing.
170: */
171: protected Document validateResource(ErrorHandler handler,
172: InputStream stream) throws ParserConfigurationException,
173: SAXException, IOException {
174:
175: DocumentBuilderFactory dbf = DocumentBuilderFactory
176: .newInstance();
177: dbf.setNamespaceAware(true);
178:
179: // Set schema location only if we found one inside the classpath.
180: Resource schemaLocation = findSchemaResource();
181: if (schemaLocation != null) {
182: if (logger.isDebugEnabled()) {
183: logger.debug("Found schema resource: "
184: + schemaLocation.getURL());
185: }
186: dbf.setValidating(true);
187: dbf.setAttribute(JAXP_SCHEMA_LANGUAGE,
188: XMLConstants.W3C_XML_SCHEMA_NS_URI);
189: dbf.setAttribute(JAXP_SCHEMA_SOURCE, schemaLocation
190: .getURL().toString());
191: } else {
192: logger
193: .debug("Schema resource ["
194: + SCHEMA_NAME
195: + "] not found - falling back to XML parsing without schema validation");
196: }
197:
198: DocumentBuilder parser = dbf.newDocumentBuilder();
199: parser.setErrorHandler(handler);
200: return parser.parse(stream);
201: }
202:
203: /**
204: * Try to locate the schema first in the class path before using the URL specified inside the XML.
205: * @return an existing resource, or <code>null</code> if none found
206: */
207: protected Resource findSchemaResource() {
208: for (int i = 0; i < SCHEMA_RESOURCE_LOCATIONS.length; i++) {
209: Resource schemaLocation = this .resourcePatternResolver
210: .getResource(SCHEMA_RESOURCE_LOCATIONS[i]);
211: if (schemaLocation.exists()) {
212: return schemaLocation;
213: }
214: }
215: return null;
216: }
217:
218: /**
219: * Parse the validated document and populates(add to) the given unit info
220: * list.
221: */
222: protected List<SpringPersistenceUnitInfo> parseDocument(
223: Resource resource, Document document,
224: List<SpringPersistenceUnitInfo> infos) throws IOException {
225:
226: Element persistence = document.getDocumentElement();
227: URL unitRootURL = determinePersistenceUnitRootUrl(resource);
228: List<Element> units = (List<Element>) DomUtils
229: .getChildElementsByTagName(persistence,
230: PERSISTENCE_UNIT);
231: for (Element unit : units) {
232: SpringPersistenceUnitInfo info = parsePersistenceUnitInfo(unit);
233: info.setPersistenceUnitRootUrl(unitRootURL);
234: infos.add(info);
235: }
236:
237: return infos;
238: }
239:
240: /**
241: * Determine the persistence unit root URL based on the given resource
242: * (which points to the <code>persistence.xml</code> file we're reading).
243: * @param resource the resource to check
244: * @return the corresponding persistence unit root URL
245: * @throws IOException if the checking failed
246: */
247: protected URL determinePersistenceUnitRootUrl(Resource resource)
248: throws IOException {
249: URL originalURL = resource.getURL();
250: String urlToString = originalURL.toExternalForm();
251:
252: // If we get an archive, simply return the jar URL (section 6.2 from the JPA spec)
253: if (ResourceUtils.isJarURL(originalURL)) {
254: return ResourceUtils.extractJarFileURL(originalURL);
255: }
256:
257: else {
258: // check META-INF folder
259: if (!urlToString.contains(META_INF)) {
260: if (logger.isInfoEnabled()) {
261: logger
262: .info(resource.getFilename()
263: + " should be located inside META-INF directory; cannot determine persistence unit root URL for "
264: + resource);
265: }
266: return null;
267: }
268: if (urlToString.lastIndexOf(META_INF) == urlToString
269: .lastIndexOf('/')
270: - (1 + META_INF.length())) {
271: if (logger.isInfoEnabled()) {
272: logger
273: .info(resource.getFilename()
274: + " is not located in the root of META-INF directory; cannot determine persistence unit root URL for "
275: + resource);
276: }
277: return null;
278: }
279:
280: String persistenceUnitRoot = urlToString.substring(0,
281: urlToString.lastIndexOf(META_INF));
282: return new URL(persistenceUnitRoot);
283: }
284: }
285:
286: /**
287: * Parse the unit info DOM element.
288: */
289: protected SpringPersistenceUnitInfo parsePersistenceUnitInfo(
290: Element persistenceUnit) throws IOException {
291: SpringPersistenceUnitInfo unitInfo = new SpringPersistenceUnitInfo();
292:
293: // set unit name
294: unitInfo.setPersistenceUnitName(persistenceUnit.getAttribute(
295: UNIT_NAME).trim());
296:
297: // set transaction type
298: String txType = persistenceUnit.getAttribute(TRANSACTION_TYPE)
299: .trim();
300: if (StringUtils.hasText(txType)) {
301: unitInfo.setTransactionType(PersistenceUnitTransactionType
302: .valueOf(txType));
303: }
304:
305: // data-source
306: String jtaDataSource = DomUtils.getChildElementValueByTagName(
307: persistenceUnit, JTA_DATA_SOURCE);
308: if (StringUtils.hasText(jtaDataSource)) {
309: unitInfo.setJtaDataSource(this .dataSourceLookup
310: .getDataSource(jtaDataSource.trim()));
311: }
312:
313: String nonJtaDataSource = DomUtils
314: .getChildElementValueByTagName(persistenceUnit,
315: NON_JTA_DATA_SOURCE);
316: if (StringUtils.hasText(nonJtaDataSource)) {
317: unitInfo.setNonJtaDataSource(this .dataSourceLookup
318: .getDataSource(nonJtaDataSource.trim()));
319: }
320:
321: // provider
322: String provider = DomUtils.getChildElementValueByTagName(
323: persistenceUnit, PROVIDER);
324: if (StringUtils.hasText(provider)) {
325: unitInfo.setPersistenceProviderClassName(provider.trim());
326: }
327:
328: // exclude unlisted classes
329: Element excludeUnlistedClasses = DomUtils
330: .getChildElementByTagName(persistenceUnit,
331: EXCLUDE_UNLISTED_CLASSES);
332: if (excludeUnlistedClasses != null) {
333: unitInfo.setExcludeUnlistedClasses(true);
334: }
335:
336: // mapping file
337: parseMappingFiles(persistenceUnit, unitInfo);
338: parseJarFiles(persistenceUnit, unitInfo);
339: parseClass(persistenceUnit, unitInfo);
340: parseProperty(persistenceUnit, unitInfo);
341: return unitInfo;
342: }
343:
344: /**
345: * Parse the <code>property</code> XML elements.
346: */
347: @SuppressWarnings("unchecked")
348: protected void parseProperty(Element persistenceUnit,
349: SpringPersistenceUnitInfo unitInfo) {
350: Element propRoot = DomUtils.getChildElementByTagName(
351: persistenceUnit, PROPERTIES);
352: if (propRoot == null) {
353: return;
354: }
355: List<Element> properties = DomUtils.getChildElementsByTagName(
356: propRoot, "property");
357: for (Element property : properties) {
358: String name = property.getAttribute("name");
359: String value = property.getAttribute("value");
360: unitInfo.addProperty(name, value);
361: }
362: }
363:
364: /**
365: * Parse the <code>class</code> XML elements.
366: */
367: @SuppressWarnings("unchecked")
368: protected void parseClass(Element persistenceUnit,
369: SpringPersistenceUnitInfo unitInfo) {
370: List<Element> classes = DomUtils.getChildElementsByTagName(
371: persistenceUnit, MANAGED_CLASS_NAME);
372: for (Element element : classes) {
373: String value = DomUtils.getTextValue(element).trim();
374: if (StringUtils.hasText(value))
375: unitInfo.addManagedClassName(value);
376: }
377: }
378:
379: /**
380: * Parse the <code>jar-file</code> XML elements.
381: */
382: @SuppressWarnings("unchecked")
383: protected void parseJarFiles(Element persistenceUnit,
384: SpringPersistenceUnitInfo unitInfo) throws IOException {
385: List<Element> jars = DomUtils.getChildElementsByTagName(
386: persistenceUnit, JAR_FILE_URL);
387: for (Element element : jars) {
388: String value = DomUtils.getTextValue(element).trim();
389: if (StringUtils.hasText(value)) {
390: Resource resource = this .resourcePatternResolver
391: .getResource(value);
392: unitInfo.addJarFileUrl(resource.getURL());
393: }
394: }
395: }
396:
397: /**
398: * Parse the <code>mapping-file</code> XML elements.
399: */
400: @SuppressWarnings("unchecked")
401: protected void parseMappingFiles(Element persistenceUnit,
402: SpringPersistenceUnitInfo unitInfo) {
403: List<Element> files = DomUtils.getChildElementsByTagName(
404: persistenceUnit, MAPPING_FILE_NAME);
405: for (Element element : files) {
406: String value = DomUtils.getTextValue(element).trim();
407: if (StringUtils.hasText(value))
408: unitInfo.addMappingFileName(value);
409: }
410: }
411:
412: }
|