View Javadoc
1   package org.synchronoss.cpo.core.meta;
2   
3   /*-
4    * [[
5    * core
6    * ==
7    * Copyright (C) 2003 - 2026 Exaxis LLC, Synchronoss Technologies Inc
8    * ==
9    * This program is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation, either version 3 of the
12   * License, or (at your option) any later version.
13   *
14   * This program is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17   * GNU General Lesser Public License for more details.
18   *
19   * You should have received a copy of the GNU General Lesser Public
20   * License along with this program.  If not, see
21   * <http://www.gnu.org/licenses/lgpl-3.0.html>.
22   * ]]
23   */
24  
25  import jakarta.xml.bind.JAXBContext;
26  import jakarta.xml.bind.JAXBElement;
27  import jakarta.xml.bind.JAXBException;
28  import jakarta.xml.bind.Marshaller;
29  import java.io.File;
30  import java.io.OutputStream;
31  import java.io.Writer;
32  import java.lang.reflect.Constructor;
33  import java.lang.reflect.InvocationTargetException;
34  import java.util.ArrayList;
35  import java.util.Collection;
36  import java.util.Collections;
37  import java.util.List;
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  import org.synchronoss.cpo.core.CpoException;
41  import org.synchronoss.cpo.core.cache.CpoMetaDescriptorCache;
42  import org.synchronoss.cpo.core.exporter.CoreMetaXmlObjectExporter;
43  import org.synchronoss.cpo.core.exporter.MetaXmlObjectExporter;
44  import org.synchronoss.cpo.core.helper.CpoClassLoader;
45  import org.synchronoss.cpo.core.helper.ExceptionHelper;
46  import org.synchronoss.cpo.core.helper.XmlHelper;
47  import org.synchronoss.cpo.core.meta.domain.*;
48  import org.synchronoss.cpo.core.parser.ExpressionParser;
49  import org.synchronoss.cpo.cpometa.CtCpoMetaData;
50  import org.synchronoss.cpo.cpometa.ObjectFactory;
51  
52  /**
53   * {@code CpoMetaDescriptor} is the runtime, named handle on a loaded CPO metadata source: it owns
54   * the datastore-specific {@link AbstractCpoMetaAdapter} that holds the actual {@link CpoClass}
55   * metadata, and delegates the {@link CpoMetaAdapter} contract to it. Instances are singletons per
56   * name, cached in the inherited {@link CpoMetaDescriptorCache} and looked up/created via the static
57   * {@code getInstance}/{@code createUpdateInstance} factory methods rather than constructed
58   * directly.
59   *
60   * <p>A descriptor can be built up from one or more meta XML documents (via {@link
61   * #getInstance(String, List, boolean)} and friends); loading multiple documents into the same named
62   * descriptor merges their {@link CpoClass} definitions, which is what enables the polymorphic
63   * override pattern described in the project documentation.
64   *
65   * @author dberry
66   */
67  public class CpoMetaDescriptor extends CpoMetaDescriptorCache
68      implements CpoMetaAdapter, CpoMetaExportable {
69    private static final Logger logger = LoggerFactory.getLogger(CpoMetaDescriptor.class);
70    private String name = null;
71    private boolean caseSensitive = true;
72    private AbstractCpoMetaAdapter metaAdapter = null;
73  
74    // used by cpo util
75    private String defaultPackageName;
76  
77    private CpoMetaDescriptor() {}
78  
79    protected CpoMetaDescriptor(String name, boolean caseSensitive) throws CpoException {
80      this.name = name;
81      this.caseSensitive = caseSensitive;
82  
83      // Lets create the metaAdapter
84      try {
85        Class<?> metaAdapterClass = getMetaAdapterClass();
86        logger.debug("Creating MetaAdapter: " + metaAdapterClass.getName());
87        metaAdapter =
88            (AbstractCpoMetaAdapter) metaAdapterClass.getDeclaredConstructor().newInstance();
89        logger.debug("Created MetaAdapter: " + metaAdapterClass.getName());
90      } catch (InstantiationException ie) {
91        throw new CpoException("Could not instantiate CpoMetaAdapter: ", ie);
92      } catch (IllegalAccessException iae) {
93        throw new CpoException("Could not access CpoMetaAdapter: ", iae);
94      } catch (ClassCastException cce) {
95        throw new CpoException(
96            "CpoMetaAdapter must extend AbstractCpoMetaAdapter: "
97                + getMetaAdapterClass().getName()
98                + ":",
99            cce);
100     } catch (InvocationTargetException e) {
101       throw new CpoException(
102           "Could not invoke constructor for CpoMetaAdapter: "
103               + getMetaAdapterClass().getName()
104               + ":",
105           e);
106     } catch (NoSuchMethodException e) {
107       throw new CpoException(
108           "Could not find a default constructor for CpoMetaAdapter: "
109               + getMetaAdapterClass().getName()
110               + ":",
111           e);
112     }
113   }
114 
115   protected Class<?> getMetaAdapterClass() throws CpoException {
116     throw new CpoException("getMetaAdapterClass() must be implemented");
117   }
118 
119   /**
120    * Gets whether {@code metaDescriptor} is registered in the descriptor cache under its own name.
121    *
122    * @param metaDescriptor the descriptor to check
123    * @return {@code true} if a descriptor with the same name is currently cached, {@code false}
124    *     otherwise
125    */
126   public static boolean isValidMetaDescriptor(CpoMetaDescriptor metaDescriptor) {
127     return findCpoMetaDescriptor(metaDescriptor.getName()) != null;
128   }
129 
130   /**
131    * Gets the already-loaded descriptor registered under the given name.
132    *
133    * @param name the descriptor name
134    * @return the cached descriptor, or {@code null} if none is registered under {@code name}
135    * @throws CpoException if the descriptor cannot be looked up
136    */
137   public static CpoMetaDescriptor getInstance(String name) throws CpoException {
138     return findCpoMetaDescriptor(name);
139   }
140 
141   /**
142    * Removes the descriptor registered under the given name from the cache.
143    *
144    * @param name the descriptor name
145    * @throws CpoException if the descriptor cannot be removed
146    */
147   public static void removeInstance(String name) throws CpoException {
148     removeCpoMetaDescriptor(name);
149   }
150 
151   /**
152    * Removes all descriptors from the cache.
153    *
154    * @throws CpoException if the cache cannot be cleared
155    */
156   public static void clearAllInstances() throws CpoException {
157     clearCpoMetaDescriptorCache();
158   }
159 
160   /**
161    * Gets the descriptor registered under {@code name}, creating and loading it from a single meta
162    * XML document if it does not already exist.
163    *
164    * @param name the descriptor name
165    * @param metaXml the meta XML document (or classpath/file reference) to load
166    * @param caseSensitive whether attribute data names should be matched case-sensitively
167    * @return the loaded (or previously cached) descriptor
168    * @throws CpoException if the meta XML cannot be parsed or loaded
169    */
170   public static CpoMetaDescriptor getInstance(String name, String metaXml, boolean caseSensitive)
171       throws CpoException {
172     List<String> metaXmls = new ArrayList<>();
173     metaXmls.add(metaXml);
174     return createUpdateInstance(name, metaXmls, caseSensitive);
175   }
176 
177   /**
178    * Gets the descriptor registered under {@code name}, creating and loading it from one or more
179    * meta XML documents if it does not already exist. Loading multiple documents into the same
180    * descriptor merges their class definitions.
181    *
182    * @param name the descriptor name
183    * @param metaXmls the meta XML documents (or classpath/file references) to load, in order
184    * @param caseSensitive whether attribute data names should be matched case-sensitively
185    * @return the loaded (or previously cached) descriptor
186    * @throws CpoException if the meta XML cannot be parsed or loaded
187    */
188   public static CpoMetaDescriptor getInstance(
189       String name, List<String> metaXmls, boolean caseSensitive) throws CpoException {
190     return createUpdateInstance(name, metaXmls, caseSensitive);
191   }
192 
193   /**
194    * Gets the descriptor registered under {@code name}, creating and loading it from one or more
195    * meta XML documents if it does not already exist. Loading multiple documents into the same
196    * descriptor merges their class definitions.
197    *
198    * @param name the descriptor name
199    * @param metaXmls the meta XML documents (or classpath/file references) to load, in order
200    * @param caseSensitive whether attribute data names should be matched case-sensitively
201    * @return the loaded (or previously cached) descriptor
202    * @throws CpoException if the meta XML cannot be parsed or loaded
203    */
204   public static CpoMetaDescriptor getInstance(String name, String[] metaXmls, boolean caseSensitive)
205       throws CpoException {
206     return createUpdateInstance(name, metaXmls, caseSensitive);
207   }
208 
209   /**
210    * Gets the names of all meta descriptors currently loaded.
211    *
212    * @return A collection of names of all meta descriptors currently loaded
213    */
214   public static Collection<String> getCpoMetaDescriptorNames() {
215     return CpoMetaDescriptorCache.getCpoMetaDescriptorNames();
216   }
217 
218   /**
219    * Hot-reloads the descriptor registered under {@code name} by merging in the given meta XML
220    * documents, if that descriptor is currently loaded. A no-op if no descriptor is registered under
221    * {@code name}.
222    *
223    * @param name the descriptor name
224    * @param metaXmls the meta XML documents (or classpath/file references) to (re)load
225    * @throws CpoException if the meta XML cannot be parsed or loaded
226    */
227   public static void refreshDescriptorMeta(String name, List<String> metaXmls) throws CpoException {
228     refreshDescriptorMeta(name, metaXmls, false);
229   }
230 
231   /**
232    * Hot-reloads the descriptor registered under {@code name} by merging in (or, if {@code
233    * overwrite} is set, replacing with) the given meta XML documents, if that descriptor is
234    * currently loaded. A no-op if no descriptor is registered under {@code name}.
235    *
236    * @param name the descriptor name
237    * @param metaXmls the meta XML documents (or classpath/file references) to (re)load
238    * @param overwrite whether to discard the descriptor's existing classes before loading
239    * @throws CpoException if the meta XML cannot be parsed or loaded
240    */
241   public static void refreshDescriptorMeta(String name, List<String> metaXmls, boolean overwrite)
242       throws CpoException {
243     CpoMetaDescriptor metaDescriptor = findCpoMetaDescriptor(name);
244     if (metaDescriptor != null) {
245       metaDescriptor.refreshDescriptorMeta(metaXmls, overwrite);
246     }
247   }
248 
249   /**
250    * Hot-reloads this descriptor by merging in the given meta XML documents.
251    *
252    * @param metaXmls the meta XML documents (or classpath/file references) to (re)load
253    * @throws CpoException if the meta XML cannot be parsed or loaded
254    */
255   public void refreshDescriptorMeta(List<String> metaXmls) throws CpoException {
256     refreshDescriptorMeta(metaXmls, false);
257   }
258 
259   /**
260    * Hot-reloads this descriptor by merging in (or, if {@code overwrite} is set, replacing with) the
261    * given meta XML documents.
262    *
263    * @param metaXmls the meta XML documents (or classpath/file references) to (re)load
264    * @param overwrite whether to discard this descriptor's existing classes before loading
265    * @throws CpoException if the meta XML cannot be parsed or loaded
266    */
267   public void refreshDescriptorMeta(List<String> metaXmls, boolean overwrite) throws CpoException {
268     if (overwrite) {
269       getCpoMetaAdapter().removeAllCpoClass();
270     }
271     createUpdateInstance(this.getName(), metaXmls, caseSensitive);
272   }
273 
274   protected static CpoMetaDescriptor createUpdateInstance(
275       String name, List<String> metaXmls, boolean caseSensitive) throws CpoException {
276     return createUpdateInstance(name, metaXmls.toArray(new String[0]), caseSensitive);
277   }
278 
279   protected static CpoMetaDescriptor createUpdateInstance(
280       String name, String[] metaXmls, boolean caseSensitive) throws CpoException {
281     CpoMetaDescriptor metaDescriptor = findCpoMetaDescriptor(name);
282     String metaDescriptorClassName = null;
283     var errBuilder = new StringBuilder();
284 
285     logger.debug("CpoMetaDescriptor: " + metaDescriptor);
286 
287     for (String metaXml : metaXmls) {
288       logger.debug("Processing: " + metaXml);
289       errBuilder.setLength(0);
290       CtCpoMetaData ctCpoMetaData =
291           XmlHelper.unmarshalXmlObject(
292               XmlHelper.CPO_META_XSD, metaXml, CtCpoMetaData.class, errBuilder);
293       if (!errBuilder.isEmpty()) {
294         throw new RuntimeException("Error parsing CPO meta XML: " + errBuilder.toString());
295       }
296 
297       try {
298         if (metaDescriptor == null) {
299           logger.debug("Getting descriptor name");
300           metaDescriptorClassName = ctCpoMetaData.getMetaDescriptor();
301           logger.debug("Getting the Class");
302           Class<?> clazz = CpoClassLoader.forName(metaDescriptorClassName);
303           logger.debug("Getting the Constructor");
304           Constructor<?> cons = clazz.getConstructor(String.class, boolean.class);
305           logger.debug("Creating the instance");
306           metaDescriptor = (CpoMetaDescriptor) cons.newInstance(name, caseSensitive);
307           logger.debug("Adding the MetaDescriptor");
308           addCpoMetaDescriptor(metaDescriptor);
309         } else if (!metaDescriptor.getClass().getName().equals(ctCpoMetaData.getMetaDescriptor())) {
310           throw new CpoException(
311               "Error processing multiple metaXml files. All files must have the same"
312                   + " CpoMetaDescriptor class name.");
313         }
314 
315         metaDescriptor.setDefaultPackageName(ctCpoMetaData.getDefaultPackageName());
316         metaDescriptor.getCpoMetaAdapter().loadCpoMetaDataDocument(ctCpoMetaData, caseSensitive);
317       } catch (ClassNotFoundException cnfe) {
318         throw new CpoException(
319             "CpoMetaAdapter not found: "
320                 + metaDescriptorClassName
321                 + ": "
322                 + ExceptionHelper.getLocalizedMessage(cnfe));
323       } catch (IllegalAccessException iae) {
324         throw new CpoException(
325             "Could not access CpoMetaAdapter: "
326                 + metaDescriptorClassName
327                 + ": "
328                 + ExceptionHelper.getLocalizedMessage(iae));
329       } catch (InstantiationException ie) {
330         throw new CpoException(
331             "Could not instantiate CpoMetaAdapter: "
332                 + metaDescriptorClassName
333                 + ": "
334                 + ExceptionHelper.getLocalizedMessage(ie));
335       } catch (InvocationTargetException ite) {
336         throw new CpoException(
337             "Could not invoke constructor: "
338                 + metaDescriptorClassName
339                 + ": "
340                 + ExceptionHelper.getLocalizedMessage(ite));
341       } catch (IllegalArgumentException iae) {
342         throw new CpoException(
343             "Illegal Argument to constructor: "
344                 + metaDescriptorClassName
345                 + ": "
346                 + ExceptionHelper.getLocalizedMessage(iae));
347       } catch (NoSuchMethodException nsme) {
348         throw new CpoException(
349             "Could not find constructor: "
350                 + metaDescriptorClassName
351                 + ": "
352                 + ExceptionHelper.getLocalizedMessage(nsme));
353       } catch (SecurityException se) {
354         throw new CpoException(
355             "Not allowed to access constructor: "
356                 + metaDescriptorClassName
357                 + ": "
358                 + ExceptionHelper.getLocalizedMessage(se));
359       } catch (ClassCastException cce) {
360         throw new CpoException(
361             "Class is not instance of CpoMetaDescriptor: "
362                 + metaDescriptorClassName
363                 + ":"
364                 + ExceptionHelper.getLocalizedMessage(cce));
365       }
366     }
367 
368     return metaDescriptor;
369   }
370 
371   protected static CpoMetaDescriptor createUpdateInstance(
372       CpoMetaDescriptor metaDescriptor, CpoMetaAdapter metaAdapter) throws CpoException {
373     if (metaDescriptor != null && metaAdapter != null) {
374       addCpoMetaDescriptor(metaDescriptor);
375     }
376     return metaDescriptor;
377   }
378 
379   protected AbstractCpoMetaAdapter getCpoMetaAdapter() {
380     return metaAdapter;
381   }
382 
383   /** {@inheritDoc} */
384   @Override
385   public <T> CpoClass getMetaClass(T bean) throws CpoException {
386     CpoClass cpoClass = getCpoMetaAdapter().getMetaClass(bean);
387     if (cpoClass != null) {
388       cpoClass.loadRunTimeInfo(this);
389     }
390 
391     return cpoClass;
392   }
393 
394   /** {@inheritDoc} */
395   @Override
396   public List<CpoClass> getCpoClasses() throws CpoException {
397     return getCpoMetaAdapter().getCpoClasses();
398   }
399 
400   /**
401    * Adds a {@link CpoClass} to this descriptor's metadata.
402    *
403    * @param cpoClass the class metadata to add
404    * @throws CpoException if the class cannot be added
405    */
406   public void addCpoClass(CpoClass cpoClass) throws CpoException {
407     getCpoMetaAdapter().addCpoClass(cpoClass);
408   }
409 
410   /**
411    * Removes a {@link CpoClass} from this descriptor's metadata.
412    *
413    * @param cpoClass the class metadata to remove
414    * @throws CpoException if the class cannot be removed
415    */
416   public void removeCpoClass(CpoClass cpoClass) throws CpoException {
417     getCpoMetaAdapter().removeCpoClass(cpoClass);
418   }
419 
420   /** {@inheritDoc} */
421   @Override
422   public ExpressionParser getExpressionParser() throws CpoException {
423     return getCpoMetaAdapter().getExpressionParser();
424   }
425 
426   /** {@inheritDoc} */
427   @Override
428   public String getDataTypeName(CpoAttribute attribute) throws CpoException {
429     return getCpoMetaAdapter().getDataTypeName(attribute);
430   }
431 
432   /** {@inheritDoc} */
433   @Override
434   public Class<?> getDataTypeJavaClass(CpoAttribute attribute) throws CpoException {
435     return getCpoMetaAdapter().getDataTypeJavaClass(attribute);
436   }
437 
438   /** {@inheritDoc} */
439   @Override
440   public int getDataTypeInt(String dataTypeName) throws CpoException {
441     return getCpoMetaAdapter().getDataTypeInt(dataTypeName);
442   }
443 
444   /** {@inheritDoc} */
445   @Override
446   public DataTypeMapEntry<?> getDataTypeMapEntry(int dataTypeInt) throws CpoException {
447     return getCpoMetaAdapter().getDataTypeMapEntry(dataTypeInt);
448   }
449 
450   /** {@inheritDoc} */
451   @Override
452   public List<String> getAllowableDataTypes() throws CpoException {
453     return getCpoMetaAdapter().getAllowableDataTypes();
454   }
455 
456   /**
457    * Creates a new, empty {@link CpoClass} of the concrete type appropriate for this descriptor's
458    * case-sensitivity setting.
459    *
460    * @return a new, unpopulated class metadata instance
461    * @throws CpoException if the instance cannot be created
462    */
463   public CpoClass createCpoClass() throws CpoException {
464     return getCpoMetaAdapter().createCpoClass(caseSensitive);
465   }
466 
467   /**
468    * Creates a new, empty {@link CpoAttribute}.
469    *
470    * @return a new, unpopulated attribute metadata instance
471    * @throws CpoException if the instance cannot be created
472    */
473   public CpoAttribute createCpoAttribute() throws CpoException {
474     return getCpoMetaAdapter().createCpoAttribute();
475   }
476 
477   /**
478    * Creates a new, empty {@link CpoFunctionGroup}.
479    *
480    * @return a new, unpopulated function group metadata instance
481    * @throws CpoException if the instance cannot be created
482    */
483   public CpoFunctionGroup createCpoFunctionGroup() throws CpoException {
484     return getCpoMetaAdapter().createCpoFunctionGroup();
485   }
486 
487   /**
488    * Creates a new, empty {@link CpoFunction}.
489    *
490    * @return a new, unpopulated function metadata instance
491    * @throws CpoException if the instance cannot be created
492    */
493   public CpoFunction createCpoFunction() throws CpoException {
494     return getCpoMetaAdapter().createCpoFunction();
495   }
496 
497   /**
498    * Creates a new, empty {@link CpoArgument}.
499    *
500    * @return a new, unpopulated argument metadata instance
501    * @throws CpoException if the instance cannot be created
502    */
503   public CpoArgument createCpoArgument() throws CpoException {
504     return getCpoMetaAdapter().createCpoArgument();
505   }
506 
507   /**
508    * Gets the default Java package name used by the CPO tooling (e.g. {@code cpo-plugin} code
509    * generation) when a meta XML document does not otherwise specify one.
510    *
511    * @return the default package name, or {@code null} if none has been set
512    */
513   public String getDefaultPackageName() {
514     return defaultPackageName;
515   }
516 
517   /**
518    * Sets the default Java package name used by the CPO tooling. A {@code null} argument is ignored,
519    * leaving any previously set value unchanged.
520    *
521    * @param packageName the default package name
522    */
523   public void setDefaultPackageName(String packageName) {
524     if (packageName != null) {
525       defaultPackageName = packageName;
526     }
527   }
528 
529   /**
530    * Gets the name this descriptor is registered under.
531    *
532    * @return the descriptor name
533    */
534   public String getName() {
535     return name;
536   }
537 
538   protected MetaXmlObjectExporter getMetaXmlObjectExporter() {
539     return new CoreMetaXmlObjectExporter(this);
540   }
541 
542   protected final CtCpoMetaData buildCpoMetaData() {
543     MetaXmlObjectExporter metaXmlObjectExporter = getMetaXmlObjectExporter();
544 
545     // need these sorted
546     List<CpoClass> classList = new ArrayList<>();
547     classList.addAll(getCpoMetaAdapter().getCpoClasses());
548     Collections.sort(classList);
549     for (CpoClass cpoClass : classList) {
550       cpoClass.acceptMetaDFVisitor(metaXmlObjectExporter);
551     }
552     return metaXmlObjectExporter.getCpoMetaData();
553   }
554 
555   private JAXBElement<CtCpoMetaData> getJaxbElement(CtCpoMetaData ctCpoMetaData) {
556     ObjectFactory factory = new ObjectFactory();
557     return factory.createCpoMetaData(ctCpoMetaData);
558   }
559 
560   private Marshaller createMarshaller() throws JAXBException {
561     JAXBContext jaxbContext = JAXBContext.newInstance(CtCpoMetaData.class);
562     Marshaller marshaller = jaxbContext.createMarshaller();
563     XmlHelper.setMarshallerProperties(marshaller);
564     return marshaller;
565   }
566 
567   /** {@inheritDoc} */
568   @Override
569   public final void export(File file) throws CpoException {
570     try {
571       CtCpoMetaData ctCpoMetaData = buildCpoMetaData();
572       Marshaller marshaller = createMarshaller();
573       marshaller.marshal(getJaxbElement(ctCpoMetaData), file);
574     } catch (JAXBException ex) {
575       throw new CpoException(ex);
576     }
577   }
578 
579   /** {@inheritDoc} */
580   @Override
581   public final void export(Writer writer) throws CpoException {
582     try {
583       CtCpoMetaData ctCpoMetaData = buildCpoMetaData();
584       Marshaller marshaller = createMarshaller();
585       marshaller.marshal(getJaxbElement(ctCpoMetaData), writer);
586     } catch (JAXBException ex) {
587       throw new CpoException(ex);
588     }
589   }
590 
591   /** {@inheritDoc} */
592   @Override
593   public final void export(OutputStream outputStream) throws CpoException {
594     try {
595       CtCpoMetaData ctCpoMetaData = buildCpoMetaData();
596       Marshaller marshaller = createMarshaller();
597       marshaller.marshal(getJaxbElement(ctCpoMetaData), outputStream);
598     } catch (JAXBException ex) {
599       throw new CpoException(ex);
600     }
601   }
602 
603   /**
604    * Gets whether this descriptor matches attribute data names case-sensitively.
605    *
606    * @return {@code true} if data names are matched case-sensitively, {@code false} otherwise
607    */
608   public boolean isCaseSensitive() {
609     return caseSensitive;
610   }
611 }