View Javadoc

1   package net.sf.sapjcosupport;
2   
3   import org.apache.log4j.Logger;
4   import org.springframework.core.io.ClassPathResource;
5   import org.springframework.util.xml.DomUtils;
6   import org.w3c.dom.Document;
7   import org.w3c.dom.Element;
8   
9   import javax.xml.parsers.DocumentBuilder;
10  import javax.xml.parsers.DocumentBuilderFactory;
11  import java.util.*;
12  
13  /**
14   * This class parses a VO SAP mapping and caches it internally.
15   *
16   * @author Jo Vandermeeren
17   * @since Jan 18, 2006 - 5:34:08 PM
18   */
19  public class SapMappingParser {
20      private static final Logger logger = Logger.getLogger(SapMappingParser.class);
21  
22      /**
23       * Internal caching class for <code>SapMapping</code> instances.
24       */
25      private static class MappingCache {
26          private static Map cache;
27  
28          static {
29              cache = Collections.synchronizedMap(new HashMap());
30          }
31  
32          public static synchronized SapMapping get(Class persistentClass) {
33              return (SapMapping) cache.get(persistentClass);
34          }
35  
36          public static synchronized void register(Class persistentClass, SapMapping mapping) {
37              if (logger.isInfoEnabled()) {
38                  logger.info("Caching mapping for entity " + persistentClass.getName());
39              }
40              cache.put(persistentClass, mapping);
41          }
42      }
43  
44      /**
45       * This method retrieves a cached mapping for a given value object class (VO).
46       * If it doesn.t exist in cache, it will be looked up, parsed, cached and returned.
47       *
48       * @param clazz VO class
49       * @return SAP mapping for the given class
50       * @throws SapJcoMappingException if something goes wrong when creating the SAP mapping
51       */
52      public synchronized static SapMapping getInfo(Class clazz) throws SapJcoMappingException {
53          SapMapping mapping = MappingCache.get(clazz);
54          if (mapping == null) {
55              if (logger.isInfoEnabled()) {
56                  logger.info("Mapping for entity '" + clazz.getName() + "' not cached yet, parsing mapping definition");
57              }
58              mapping = parseMapping(clazz);
59              MappingCache.register(clazz, mapping);
60          }
61          return mapping;
62      }
63  
64      /**
65       * This helper method tries to find the SAP JCO mapping file in the same classpath location as the
66       * <code>clazz</code> class definition. The mapping file has the same name as the class and ends with ".sap.xml".
67       * This file is read and parsed to a <code>SapMapping</code> object.
68       *
69       * @param clazz the VO class
70       * @return SAP mapping
71       * @throws SapJcoMappingException if something goes wrong when creating the SAP mapping
72       */
73      private static SapMapping parseMapping(Class clazz) throws SapJcoMappingException {
74          try {
75              String fqName = clazz.getName();
76              String resource = fqName.replace('.', '/');
77              String shortName = fqName.substring(fqName.lastIndexOf(".") + 1);
78              ClassPathResource classPathResource = new ClassPathResource(resource + ".sap.xml");
79  
80              DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
81              Document mappingXml = builder.parse(classPathResource.getInputStream());
82  
83              Element root = mappingXml.getDocumentElement();
84  
85              List classDefs = DomUtils.getChildElementsByTagName(root, "class");
86              if (classDefs == null || classDefs.size() == 0) {
87                  throw new SapJcoMappingException(
88                          "SAP Mapping xml '" + shortName + ".sap.xml' should contain 1 <class/> definition");
89              }
90              Element classDef = (Element) classDefs.get(0);
91              String className = classDef.getAttribute("name");
92              String bapiName = classDef.getAttribute("bapi");
93              if (className == null || className.trim().length() == 0) {
94                  throw new SapJcoMappingException("'name' attribute of 'class' element is required");
95              }
96  
97              SapMapping sapMapping = new SapMapping(className);
98  
99              boolean fieldSelectionExists = false;
100             // Check if a lists elements is available and if available process the input and output list
101             List lists = DomUtils.getChildElementsByTagName(root, "list");
102             for (int i = 0; i < lists.size(); i++) {
103                 Element listsElement = (Element) lists.get(i);
104                 List inputLists = DomUtils.getChildElementsByTagName(listsElement, "inputList");
105                 for (Iterator inputListIterator = inputLists.iterator(); inputListIterator.hasNext();) {
106                     Element inputListElement = (Element) inputListIterator.next();
107                     String[] listAttributes = extractListElementAttributes(inputListElement);
108                     if (listAttributes != null) {
109                         sapMapping.addInputList(new SapListMapping(listAttributes[0], listAttributes[1]));
110                     }
111                 }
112                 String[] listAttributes = selectAndExtractListElementAttributes(listsElement, "outputList");
113                 if (listAttributes != null) {
114                     sapMapping.setOutputList(new SapListMapping(listAttributes[0], listAttributes[1]));
115                 }
116                 listAttributes = selectAndExtractListElementAttributes(listsElement, "fieldSelection");
117                 if (listAttributes != null) {
118                     fieldSelectionExists = true;
119                     sapMapping.setFieldSelection(new SapListMapping(listAttributes[0], listAttributes[1]));
120                 }
121             }
122 
123             Map fieldSelectionMap = new HashMap();
124             if (bapiName == null || bapiName.length() == 0) {
125                 throw new SapJcoMappingException("'bapi' attribute of 'class' element is required");
126             }
127             SapBapiMapping bapi = new SapBapiMapping(bapiName);
128 
129             //loop over structures
130             List structureList = DomUtils.getChildElementsByTagName(classDef, "structure");
131             for (int j = 0; j < structureList.size(); j++) {
132                 Element structureElement = (Element) structureList.get(j);
133                 String structureName = structureElement.getAttribute("name");
134                 if (structureName == null || structureName.length() == 0) {
135                     throw new SapJcoMappingException("'name' attribute of 'structure' element is required");
136                 }
137                 String mappingType = structureElement.getAttribute("type");
138                 SapStructureMapping structure;
139 
140                 if ("list".equalsIgnoreCase(mappingType)) {
141                     String keyField = structureElement.getAttribute("key-field");
142                     String primaryKey = structureElement.getAttribute("pk");
143                     if (keyField != null && keyField.trim().length() > 0 && primaryKey != null && primaryKey.trim()
144                             .length() > 0) {
145                         // List of Maps
146                         structure = new SapStructureMapping(structureName, SapStructureMapping.LIST_OF_MAPS);
147                         structure.setKeyField(keyField);
148                         structure.setPrimaryKey(primaryKey);
149                     } else {
150                         // List
151                         structure = new SapStructureMapping(structureName, SapStructureMapping.LIST);
152                     }
153                 } else if ("map".equalsIgnoreCase(mappingType)) {
154                     String keyField = structureElement.getAttribute("key-field");
155                     if (keyField == null || keyField.length() == 0) {
156                         throw new SapJcoMappingException(
157                                 "'key-field' attribute of 'structure' element is required when 'type' is 'map'");
158                     }
159                     // Map
160                     structure = new SapStructureMapping(structureName, SapStructureMapping.MAP);
161                     structure.setKeyField(keyField);
162                 } else if ("entity".equalsIgnoreCase(mappingType)) {
163                     String entityClass = structureElement.getAttribute("class");
164                     String collection = structureElement.getAttribute("collection");
165                     structure = new SapStructureMapping(structureName, SapStructureMapping.ENTITY);
166                     structure.setCollectionName(collection);
167                     structure.setEntityClass(entityClass);
168                 } else {
169                     structure = new SapStructureMapping(structureName);
170                 }
171 
172                 // This attribute defines if we're going to skip this structure when processing the output from SAP.
173                 // Structures that do not contain return values, should usually be skipped when mapping value objects.
174                 String excluded = structureElement.getAttribute("excluded");
175                 if ("true".equalsIgnoreCase(excluded)) {
176                     structure.setExcluded(true);
177                 }
178 
179                 //loop over properties
180                 List propertyList = DomUtils.getChildElementsByTagName(structureElement, "property");
181                 for (int k = 0; k < propertyList.size(); k++) {
182                     Element propertyElement = (Element) propertyList.get(k);
183                     String propertyName = propertyElement.getAttribute("name");
184                     String fieldName = propertyElement.getAttribute("field");
185                     String key = propertyElement.getAttribute("key");
186                     String type = propertyElement.getAttribute("type");
187                     String not_null = propertyElement.getAttribute("not-null");
188                     if (fieldSelectionExists) {
189                         String profiles = propertyElement.getAttribute("profiles");
190                         if (profiles != null) {
191                             StringTokenizer st = new StringTokenizer(profiles, ",", false);
192                             while (st.hasMoreTokens()) {
193                                 String profile = st.nextToken();
194                                 List selection = (List) fieldSelectionMap.get(profile);
195                                 if (selection == null) {
196                                     selection = new ArrayList();
197                                 }
198                                 selection.add(fieldName);
199                                 fieldSelectionMap.put(profile, selection);
200                             }
201                         }
202                     }
203                     if (not_null == null || not_null.length() == 0) {
204                         not_null = "false";
205                     }
206                     if (!("true".equalsIgnoreCase(not_null) || "false".equalsIgnoreCase(not_null))) {
207                         throw new SapJcoMappingException(
208                                 "'not-null' attribute of 'property' element should be 'false' or 'true'");
209                     }
210                     if (propertyName == null || propertyName.length() == 0) {
211                         throw new SapJcoMappingException("'name' attribute of 'property' element is required");
212                     }
213                     if (fieldName == null || fieldName.length() == 0) {
214                         throw new SapJcoMappingException("'field' attribute of 'property' element is required");
215                     }
216                     if (type == null || type.length() == 0) {
217                         type = "string";
218                     }
219                     boolean required = Boolean.valueOf(not_null).booleanValue();
220                     if (structure.isMap() || structure.isListOfMaps()) {
221                         if (key == null || key.length() == 0) {
222                             throw new SapJcoMappingException(
223                                     "'key' attribute of 'property' element is required when the structure is of type 'map'");
224                         }
225                         structure.addField(new SapFieldMapping(propertyName, fieldName, type, key, className,
226                                                                required));
227                     } else {
228                         structure.addField(new SapFieldMapping(propertyName, fieldName, type, className, required));
229                     }
230                 }
231                 bapi.addStructure(structure);
232             }
233             //read input settings: <input name="PLANT" not-null="false" default="NC01"/>
234             List inputList = DomUtils.getChildElementsByTagName(classDef, "input");
235             for (int j = 0; j < inputList.size(); j++) {
236                 Element inputElement = (Element) inputList.get(j);
237                 String inputName = inputElement.getAttribute("name");
238                 String inputDefault = inputElement.getAttribute("default");
239                 String notNull = inputElement.getAttribute("not-null");
240                 String pk = inputElement.getAttribute("pk");
241                 if (inputName == null || inputName.length() == 0) {
242                     throw new SapJcoMappingException("'name' attribute of 'input' element is required");
243                 }
244                 boolean required = toBoolean(notNull, "false");
245                 boolean isPk = toBoolean(pk, "false");
246                 bapi.addInput(new SapInput(inputName, inputDefault, required, isPk));
247             }
248             bapi.setFieldSelection(fieldSelectionMap);
249             sapMapping.setBapi(bapi);
250 
251             return sapMapping;
252         } catch (Exception e) {
253             String msg = new StringBuffer("Error loading mapping info for entity '").append(clazz.getName()).append("'")
254                     .toString();
255             logger.error(msg, e);
256             throw new SapJcoMappingException(msg);
257         }
258     }
259 
260     /**
261      * This helper method extracts the first child element with name <code>childElement</code> from
262      * the XML element <code>parentListsElement</code> and extracts the String values that are needed to construct
263      * the input resp. output list.
264      *
265      * @param parentListsElement parent XML element
266      * @param childElement       child XML element name
267      * @return String array containing <code>name</code> attribute (at position 0) and <code>pk</code> attribute (at position 1)
268      * @throws SapJcoMappingException if the 'name' or 'pk' attribute is not available in the XML element
269      */
270     private static String[] selectAndExtractListElementAttributes(Element parentListsElement,
271                                                                   String childElement) throws SapJcoMappingException {
272         List list = DomUtils.getChildElementsByTagName(parentListsElement, childElement);
273         if (list != null && list.size() > 0) {
274             return extractListElementAttributes((Element) list.get(0));
275         }
276         return null;
277     }
278 
279     private static String[] extractListElementAttributes(Element element) {
280         if (element == null) {
281             return null;
282         }
283         String[] attributes = new String[2];
284         attributes[0] = element.getAttribute("name");
285         if (attributes[0] == null || attributes[0].trim().length() == 0) {
286             throw new SapJcoMappingException("'name' attribute of '" + element + "' element in 'list' is required");
287         }
288         attributes[1] = element.getAttribute("pk");
289         if (attributes[1] == null || attributes[1].trim().length() == 0) {
290             throw new SapJcoMappingException("'pk' attribute of '" + element + "' element in 'list' is required");
291         }
292         return attributes;
293     }
294 
295     /**
296      * Converts String "true" to boolean <code>true</code> and String "false" to boolean <code>false</code>.
297      *
298      * @param booleanValue String value that should be parsed to boolean
299      * @param defaultValue default value to use in case booleanValue is null or zero in length
300      * @return boolean that corresponds with the String parameter
301      */
302     private static boolean toBoolean(String booleanValue, String defaultValue) {
303         if (booleanValue == null || booleanValue.length() == 0) {
304             booleanValue = defaultValue;
305         }
306         if (!("true".equalsIgnoreCase(booleanValue) || "false".equalsIgnoreCase(booleanValue))) {
307             throw new SapJcoMappingException("boolean attributes should be 'false' or 'true'");
308         }
309         return Boolean.valueOf(booleanValue).booleanValue();
310     }
311 }