001: /*
002: * Copyright 2002-2006 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 SCHEMA_NAME = "persistence_1_0.xsd";
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 META_INF = "META-INF";
085:
086: private final Log logger = LogFactory.getLog(getClass());
087:
088: private final ResourcePatternResolver resourcePatternResolver;
089:
090: private final DataSourceLookup dataSourceLookup;
091:
092: /**
093: * Create a new PersistenceUnitReader.
094: * @param resourcePatternResolver the ResourcePatternResolver to use for loading resources
095: * @param dataSourceLookup the DataSourceLookup to resolve DataSource names in
096: * <code>persistence.xml</code> files against
097: */
098: public PersistenceUnitReader(
099: ResourcePatternResolver resourcePatternResolver,
100: DataSourceLookup dataSourceLookup) {
101: Assert.notNull(resourcePatternResolver,
102: "ResourceLoader must not be null");
103: Assert.notNull(dataSourceLookup,
104: "DataSourceLookup must not be null");
105: this .resourcePatternResolver = resourcePatternResolver;
106: this .dataSourceLookup = dataSourceLookup;
107: }
108:
109: /**
110: * Parse and build all persistence unit infos defined in the specified XML file(s).
111: * @param persistenceXmlLocation the resource location (can be a pattern)
112: * @return the resulting PersistenceUnitInfo instances
113: */
114: public SpringPersistenceUnitInfo[] readPersistenceUnitInfos(
115: String persistenceXmlLocation) {
116: return readPersistenceUnitInfos(new String[] { persistenceXmlLocation });
117: }
118:
119: /**
120: * Parse and build all persistence unit infos defined in the given XML files.
121: * @param persistenceXmlLocations the resource locations (can be patterns)
122: * @return the resulting PersistenceUnitInfo instances
123: */
124: public SpringPersistenceUnitInfo[] readPersistenceUnitInfos(
125: String[] persistenceXmlLocations) {
126: ErrorHandler handler = new SimpleSaxErrorHandler(logger);
127: List<SpringPersistenceUnitInfo> infos = new LinkedList<SpringPersistenceUnitInfo>();
128: String resourceLocation = null;
129: try {
130: for (int i = 0; i < persistenceXmlLocations.length; i++) {
131: Resource[] resources = this .resourcePatternResolver
132: .getResources(persistenceXmlLocations[i]);
133: for (Resource resource : resources) {
134: resourceLocation = resource.toString();
135: InputStream stream = resource.getInputStream();
136: try {
137: Document document = validateResource(handler,
138: stream);
139: parseDocument(resource, document, infos);
140: } finally {
141: stream.close();
142: }
143: }
144: }
145: } catch (IOException ex) {
146: throw new IllegalArgumentException(
147: "Cannot parse persistence unit from "
148: + resourceLocation, ex);
149: } catch (SAXException ex) {
150: throw new IllegalArgumentException(
151: "Invalid XML in persistence unit from "
152: + resourceLocation, ex);
153: } catch (ParserConfigurationException ex) {
154: throw new IllegalArgumentException(
155: "Internal error parsing persistence unit from "
156: + resourceLocation);
157: }
158:
159: return infos
160: .toArray(new SpringPersistenceUnitInfo[infos.size()]);
161: }
162:
163: /**
164: * Validate the given stream and return a valid DOM document for parsing.
165: */
166: protected Document validateResource(ErrorHandler handler,
167: InputStream stream) throws ParserConfigurationException,
168: SAXException, IOException {
169:
170: DocumentBuilderFactory dbf = DocumentBuilderFactory
171: .newInstance();
172: dbf.setNamespaceAware(true);
173:
174: // Set schema location only if we found one inside the classpath.
175: Resource schemaLocation = findSchemaResource(SCHEMA_NAME);
176: if (schemaLocation != null) {
177: if (logger.isDebugEnabled()) {
178: logger.debug("Found schema resource: "
179: + schemaLocation.getURL());
180: }
181: dbf.setValidating(true);
182: dbf.setAttribute(JAXP_SCHEMA_LANGUAGE,
183: XMLConstants.W3C_XML_SCHEMA_NS_URI);
184: dbf.setAttribute(JAXP_SCHEMA_SOURCE, schemaLocation
185: .getURL().toString());
186: } else {
187: logger
188: .debug("Schema resource ["
189: + SCHEMA_NAME
190: + "] not found - falling back to XML parsing without schema validation");
191: }
192:
193: DocumentBuilder parser = dbf.newDocumentBuilder();
194: parser.setErrorHandler(handler);
195: return parser.parse(stream);
196: }
197:
198: /**
199: * Try to locate the schema first in the class path before using the URL specified inside the XML.
200: * @return an existing resource, or <code>null</code> if none found
201: */
202: protected Resource findSchemaResource(String schemaName) {
203: try {
204: // First search the class path root (TopLink)
205: Resource schemaLocation = this .resourcePatternResolver
206: .getResource("classpath:" + schemaName);
207: if (schemaLocation.exists()) {
208: return schemaLocation;
209: }
210:
211: // Search org packages (open source provider such as Hibernate or OpenJPA)
212: Resource[] resources = this .resourcePatternResolver
213: .getResources("classpath*:org/**/" + schemaName);
214: if (resources.length > 0) {
215: return resources[0];
216: }
217:
218: // Search com packages (some commercial provider)
219: resources = this .resourcePatternResolver
220: .getResources("classpath*:com/**/" + schemaName);
221: if (resources.length > 0) {
222: return resources[0];
223: }
224:
225: // Finally, do a lookup for unpacked files.
226: // See the warning in
227: // org.springframework.core.io.support.PathMatchingResourcePatternResolver
228: // for more info on this strategy.
229: resources = this .resourcePatternResolver
230: .getResources("classpath*:**/" + schemaName);
231: if (resources.length > 0) {
232: return resources[0];
233: }
234: } catch (IOException ex) {
235: if (logger.isDebugEnabled()) {
236: logger.debug(
237: "Could not search for JPA schema resource ["
238: + schemaName + "] in class path", ex);
239: }
240: }
241: return null;
242: }
243:
244: /**
245: * Parse the validated document and populates(add to) the given unit info
246: * list.
247: */
248: protected List<SpringPersistenceUnitInfo> parseDocument(
249: Resource resource, Document document,
250: List<SpringPersistenceUnitInfo> infos) throws IOException {
251:
252: Element persistence = document.getDocumentElement();
253: URL unitRootURL = determinePersistenceUnitRootUrl(resource);
254: List<Element> units = (List<Element>) DomUtils
255: .getChildElementsByTagName(persistence,
256: PERSISTENCE_UNIT);
257: for (Element unit : units) {
258: SpringPersistenceUnitInfo info = parsePersistenceUnitInfo(unit);
259: info.setPersistenceUnitRootUrl(unitRootURL);
260: infos.add(info);
261: }
262:
263: return infos;
264: }
265:
266: /**
267: * Determine the persistence unit root URL based on the given resource
268: * (which points to the <code>persistence.xml</code> file we're reading).
269: * @param resource the resource to check
270: * @return the corresponding persistence unit root URL
271: * @throws IOException if the checking failed
272: */
273: protected URL determinePersistenceUnitRootUrl(Resource resource)
274: throws IOException {
275: URL originalURL = resource.getURL();
276: String urlToString = originalURL.toExternalForm();
277:
278: // If we get an archive, simply return the jar URL (section 6.2 from the JPA spec)
279: if (ResourceUtils.isJarURL(originalURL)) {
280: return ResourceUtils.extractJarFileURL(originalURL);
281: }
282:
283: else {
284: // check META-INF folder
285: if (!urlToString.contains(META_INF)) {
286: if (logger.isInfoEnabled()) {
287: logger
288: .info(resource.getFilename()
289: + " should be located inside META-INF directory; cannot determine persistence unit root URL for "
290: + resource);
291: }
292: return null;
293: }
294: if (urlToString.lastIndexOf(META_INF) == urlToString
295: .lastIndexOf('/')
296: - (1 + META_INF.length())) {
297: if (logger.isInfoEnabled()) {
298: logger
299: .info(resource.getFilename()
300: + " is not located in the root of META-INF directory; cannot determine persistence unit root URL for "
301: + resource);
302: }
303: return null;
304: }
305:
306: String persistenceUnitRoot = urlToString.substring(0,
307: urlToString.lastIndexOf(META_INF));
308: return new URL(persistenceUnitRoot);
309: }
310: }
311:
312: /**
313: * Parse the unit info DOM element.
314: */
315: protected SpringPersistenceUnitInfo parsePersistenceUnitInfo(
316: Element persistenceUnit) throws IOException {
317: SpringPersistenceUnitInfo unitInfo = new SpringPersistenceUnitInfo();
318:
319: // set unit name
320: unitInfo.setPersistenceUnitName(persistenceUnit.getAttribute(
321: UNIT_NAME).trim());
322:
323: // set transaction type
324: String txType = persistenceUnit.getAttribute(TRANSACTION_TYPE)
325: .trim();
326: if (StringUtils.hasText(txType)) {
327: unitInfo.setTransactionType(PersistenceUnitTransactionType
328: .valueOf(txType));
329: }
330:
331: // data-source
332: String jtaDataSource = DomUtils.getChildElementValueByTagName(
333: persistenceUnit, JTA_DATA_SOURCE);
334: if (StringUtils.hasText(jtaDataSource)) {
335: unitInfo.setJtaDataSource(this .dataSourceLookup
336: .getDataSource(jtaDataSource.trim()));
337: }
338:
339: String nonJtaDataSource = DomUtils
340: .getChildElementValueByTagName(persistenceUnit,
341: NON_JTA_DATA_SOURCE);
342: if (StringUtils.hasText(nonJtaDataSource)) {
343: unitInfo.setNonJtaDataSource(this .dataSourceLookup
344: .getDataSource(nonJtaDataSource.trim()));
345: }
346:
347: // provider
348: String provider = DomUtils.getChildElementValueByTagName(
349: persistenceUnit, PROVIDER);
350: if (StringUtils.hasText(provider)) {
351: unitInfo.setPersistenceProviderClassName(provider.trim());
352: }
353:
354: // exclude unlisted classes
355: Element excludeUnlistedClasses = DomUtils
356: .getChildElementByTagName(persistenceUnit,
357: EXCLUDE_UNLISTED_CLASSES);
358: if (excludeUnlistedClasses != null) {
359: unitInfo.setExcludeUnlistedClasses(true);
360: }
361:
362: // mapping file
363: parseMappingFiles(persistenceUnit, unitInfo);
364: parseJarFiles(persistenceUnit, unitInfo);
365: parseClass(persistenceUnit, unitInfo);
366: parseProperty(persistenceUnit, unitInfo);
367: return unitInfo;
368: }
369:
370: /**
371: * Parse the <code>property</code> XML elements.
372: */
373: @SuppressWarnings("unchecked")
374: protected void parseProperty(Element persistenceUnit,
375: SpringPersistenceUnitInfo unitInfo) {
376: Element propRoot = DomUtils.getChildElementByTagName(
377: persistenceUnit, PROPERTIES);
378: if (propRoot == null) {
379: return;
380: }
381: List<Element> properties = DomUtils.getChildElementsByTagName(
382: propRoot, "property");
383: for (Element property : properties) {
384: String name = property.getAttribute("name");
385: String value = property.getAttribute("value");
386: unitInfo.addProperty(name, value);
387: }
388: }
389:
390: /**
391: * Parse the <code>class</code> XML elements.
392: */
393: @SuppressWarnings("unchecked")
394: protected void parseClass(Element persistenceUnit,
395: SpringPersistenceUnitInfo unitInfo) {
396: List<Element> classes = DomUtils.getChildElementsByTagName(
397: persistenceUnit, MANAGED_CLASS_NAME);
398: for (Element element : classes) {
399: String value = DomUtils.getTextValue(element).trim();
400: if (StringUtils.hasText(value))
401: unitInfo.addManagedClassName(value);
402: }
403: }
404:
405: /**
406: * Parse the <code>jar-file</code> XML elements.
407: */
408: @SuppressWarnings("unchecked")
409: protected void parseJarFiles(Element persistenceUnit,
410: SpringPersistenceUnitInfo unitInfo) throws IOException {
411: List<Element> jars = DomUtils.getChildElementsByTagName(
412: persistenceUnit, JAR_FILE_URL);
413: for (Element element : jars) {
414: String value = DomUtils.getTextValue(element).trim();
415: if (StringUtils.hasText(value)) {
416: Resource resource = this .resourcePatternResolver
417: .getResource(value);
418: unitInfo.addJarFileUrl(resource.getURL());
419: }
420: }
421: }
422:
423: /**
424: * Parse the <code>mapping-file</code> XML elements.
425: */
426: @SuppressWarnings("unchecked")
427: protected void parseMappingFiles(Element persistenceUnit,
428: SpringPersistenceUnitInfo unitInfo) {
429: List<Element> files = DomUtils.getChildElementsByTagName(
430: persistenceUnit, MAPPING_FILE_NAME);
431: for (Element element : files) {
432: String value = DomUtils.getTextValue(element).trim();
433: if (StringUtils.hasText(value))
434: unitInfo.addMappingFileName(value);
435: }
436: }
437:
438: }
|