001    /*
002      GRANITE DATA SERVICES
003      Copyright (C) 2011 GRANITE DATA SERVICES S.A.S.
004    
005      This file is part of Granite Data Services.
006    
007      Granite Data Services is free software; you can redistribute it and/or modify
008      it under the terms of the GNU Library General Public License as published by
009      the Free Software Foundation; either version 2 of the License, or (at your
010      option) any later version.
011    
012      Granite Data Services is distributed in the hope that it will be useful, but
013      WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
014      FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
015      for more details.
016    
017      You should have received a copy of the GNU Library General Public License
018      along with this library; if not, see <http://www.gnu.org/licenses/>.
019    */
020    package org.granite.osgi.metadata;
021    
022    import java.io.IOException;
023    import java.io.InputStream;
024    import java.util.ArrayList;
025    import java.util.Dictionary;
026    import java.util.HashSet;
027    import java.util.Hashtable;
028    import java.util.List;
029    import java.util.Set;
030    
031    import javax.xml.parsers.DocumentBuilder;
032    import javax.xml.parsers.DocumentBuilderFactory;
033    import javax.xml.parsers.ParserConfigurationException;
034    
035    import org.granite.logging.Logger;
036    import org.granite.osgi.classloader.ServiceClassLoader;
037    import org.granite.osgi.constants.OSGIConstants;
038    import org.osgi.framework.Bundle;
039    import org.osgi.framework.BundleContext;
040    import org.osgi.framework.BundleEvent;
041    import org.osgi.framework.SynchronousBundleListener;
042    import org.osgi.service.event.Event;
043    import org.osgi.service.event.EventAdmin;
044    import org.osgi.util.tracker.ServiceTracker;
045    import org.w3c.dom.Document;
046    import org.w3c.dom.Element;
047    import org.w3c.dom.NodeList;
048    import org.xml.sax.SAXException;
049    /**
050     * Parse the Manifest of a bundle to load the GraniteDS-Service config file
051     * i.e. GraniteDS-Service: /resources/granite-osgi.xml
052     * <pre>
053     * <graniteds>
054     *      <services>
055     *              <service packages="org.graniteds.services.security" />
056     *      </services>
057     * </graniteds>
058     * </pre>
059     * @see SynchronousBundleListener
060     * @author <a href="mailto:gembin@gmail.com">gembin@gmail.com</a>
061     * @since 1.1.0
062     */
063    public class ManifestMetadataParser implements SynchronousBundleListener {
064    
065            private static final Logger log = Logger.getLogger(ManifestMetadataParser.class);
066    
067            /**
068             * the seperator of the packages which will be scanned
069             * i.e. org.granite.service.test,org.granite.service.testa.*
070             */
071            private static final String SEPERATOR=",";
072            /**
073             * Element in the metadata file which will contain any number of service Elements
074             */
075            private static final String SERVICES="services";
076            /**
077             * Element in the metadata file
078             * i.e. <service packages="org.granite.osgi.test,org.granite.osgi.test.*" />
079             */
080            private static final String SERVICE="service";
081            /**
082             * An attribute of 'service' Element in the metadata file
083             * packages of a bundle which contain some GraniteDS dataservice classes
084             * these packages will be scanned by this parser
085             * i.e. packages="org.granite.osgi.test,org.granite.osgi.test.*"
086             */
087            private static final String PROP_PACKAGES="packages";
088            /**
089             * the property key in the MANFEST.MF to specify the metadata 
090             * i.e. GraniteDS-Service: GraniteDS-INF/domain-config/granite-osgi.xml
091             * Parser only scans the bundles who have the this property key presented 
092             */
093            private static final String GRANITEDS_SERVICE="GraniteDS-Service";
094            /**
095             * the classloader which is used to load the classes in the bundles 
096             * which will provide the dataservices
097             */
098            ServiceClassLoader classLoader;
099            /**
100             * EventAdmin is used to send a event to the EventHandler 
101             * 'ServicesConfig' when a bundle is changed
102             */
103            ServiceTracker eventAdminTracker;
104            /**
105             * bundle context of Granite OSGi bundle
106             */
107            BundleContext context;
108            /**
109             * a set of qualifed Granite dataservice classes which will 
110             * be registered or unregistered during the runtime
111             */
112            Set<Class<?>> classes; 
113            /**
114             * the metadata xml file path
115             * i.e. GraniteDS-INF/granite-osgi.xml
116             */
117            String granitedsMeta;
118            /**
119             * metadata processing thread 
120             */
121            static DocumentBuilder documentBuilder;
122            private final MetadataProcessor processorThread = new MetadataProcessor();
123            /**
124             * Constructor
125             * @param context
126             */
127            public ManifestMetadataParser(BundleContext context) {
128                    this.context = context;
129            }
130            
131            private void setGraniteMeta(String granitedsMeta) {
132                    this.granitedsMeta = granitedsMeta;
133            }
134            
135            private EventAdmin getEventAdmin(){
136                    return (EventAdmin) eventAdminTracker.getService();
137            }
138            /**
139             * broadcast service change
140             * @param eventTopic
141             */
142            private void broadcastServicesChanged(String eventTopic){
143                    if(classes!=null && classes.size()>0){
144                            Dictionary<String, Object> properties = new Hashtable<String, Object>();
145                            properties.put(OSGIConstants.SERVICE_CLASS_SET, classes);
146                            EventAdmin eadmin=getEventAdmin();
147                            if(eadmin!=null){
148                                    eadmin.sendEvent(new Event(eventTopic,properties));
149                            }else{
150                                    if(log.isErrorEnabled())
151                                            log.error("EventAdmin is unavailable, cannot broadcast Event!!!");
152                            }
153                    }
154            }
155            
156            /**
157             * parse the metadata
158             * 
159             * @param bundle
160             * @param eventTopic
161             */
162        private void parseMetadata(Bundle bundle,String eventTopic){
163            if(log.isInfoEnabled())
164                    log.info(GRANITEDS_SERVICE+":"+granitedsMeta);
165            classLoader.setBundle(bundle);
166            DocumentBuilder builder = getDocumentBuilder();
167                    try {
168                            if (builder != null) {
169                                    if(granitedsMeta==null || "".equals(granitedsMeta))return;
170                                    InputStream is=bundle.getEntry(granitedsMeta).openStream();
171                                    if(is==null)return;
172                                    Document doc=builder.parse(is);
173                                    Element servicesNode=(Element) doc.getElementsByTagName(SERVICES).item(0);
174                                    NodeList services=servicesNode.getElementsByTagName(SERVICE);
175                                    for(int i=0;i<services.getLength();i++){
176                                            Element service= (Element) services.item(i);
177                                            String[] servicePackages=service.getAttribute(PROP_PACKAGES).split(SEPERATOR);
178                                            if(servicePackages!=null){
179                                                    classes.addAll(classLoader.loadClasses(servicePackages));
180                                            }else{
181                                                    throw new RuntimeException("Invalid Service at "+i);
182                                            }
183                                    }
184                               broadcastServicesChanged(eventTopic);
185                    }
186                    } catch (SAXException e) {
187                            log.error(e, "Could not parse metadata");
188                    } catch (IOException e) {
189                            log.error(e, "Could not parse metadata");
190                    }
191        }
192        /**
193         * @return DocumentBuilder
194         */
195        private synchronized static DocumentBuilder getDocumentBuilder() {
196                    try {
197                            if(documentBuilder==null)
198                             documentBuilder= DocumentBuilderFactory.newInstance().newDocumentBuilder();
199                    } catch (ParserConfigurationException e) {
200                            log.error(e, "Could not get document builder");
201                    }
202                    //DocumentBuilder is reset to the same state as when it was created
203                    documentBuilder.reset();
204                    return documentBuilder;
205            }
206        /**
207         * start to parse Metadata
208         */
209        public void start() {
210            classLoader=new ServiceClassLoader();
211                    eventAdminTracker=new ServiceTracker(context,EventAdmin.class.getName(),null);
212                    eventAdminTracker.open();
213            new Thread(processorThread).start();
214            synchronized (this) {
215                    context.addBundleListener(this);// listen to any changes in bundles.
216            }
217            if(classes==null){
218                    classes=new HashSet<Class<?>>();
219            }else{
220                    classes.clear();
221            }
222        }
223        /**
224         * stop Metadata Parser
225         */
226        public void stop(){
227            eventAdminTracker.close();
228            processorThread.stop(); // Stop the thread processing bundles.
229            context.removeBundleListener(this);
230            classLoader=null;
231            classes=null;
232        }
233        /**
234         * @param bundle
235         * @return true if the bundle has the property key 'GraniteDS-Service' presented and with value setted
236         */
237        private boolean hasDataService(Bundle bundle){
238              if(bundle==null)return false;
239              Object gsd=bundle.getHeaders().get(GRANITEDS_SERVICE);
240              if(gsd!=null)
241                      setGraniteMeta(gsd.toString());
242              return (gsd!=null || "".equals(gsd));
243        }
244        /*
245         * (non-Javadoc)
246         * @see org.osgi.framework.BundleListener#bundleChanged(org.osgi.framework.BundleEvent)
247         */
248            public void bundleChanged(BundleEvent event) {
249                    Bundle bundle=event.getBundle();
250                    // ignore own bundle GraniteDS OSGi bundle 
251                    // and GraniteDS unpowered bundle(property key 'GraniteDS-Service' is not presented)
252                    if(context.getBundle()==bundle || !hasDataService(bundle))return;
253                    switch (event.getType()) {
254                    case BundleEvent.STARTED:
255                            // Put the bundle in the queue to register dataservices
256                            processorThread.addBundle(bundle);
257                            break;
258                    case BundleEvent.STOPPING:
259                            // Put the bundle in the queue to unregister dataservices
260                            processorThread.removeBundle(bundle);
261                            break;
262                    default:
263                            break;
264                    }
265            }
266            /**
267             *
268             */
269            private class MetadataProcessor implements Runnable{
270                    private boolean hasStarted=true;
271                    private List<Bundle> bundles = new ArrayList<Bundle>();
272                    private List<Bundle> removedBundles = new ArrayList<Bundle>();
273                    private synchronized void addBundle(Bundle bundle){
274                            bundles.add(bundle);
275                        notifyAll(); // Notify the thread to force the process.
276                    }
277                    private synchronized void removeBundle(Bundle bundle){
278                            bundles.remove(bundle);
279                            removedBundles.add(bundle);
280                            notifyAll(); // Notify the thread to force the process.
281                    }
282                    /**
283                     * Stops the processor thread.
284                     */
285                    public synchronized void stop() {
286                            hasStarted = false;
287                            bundles.clear();
288                            notifyAll();
289                    }
290                    public void run() {
291                             while (hasStarted) {
292                                     Bundle bundle=null;
293                                     Bundle removeBundle=null;
294                         synchronized (this) {
295                         while (hasStarted && bundles.isEmpty() && removedBundles.isEmpty()) {
296                            try {
297                                    //log.info("waiting...");
298                                wait();
299                            } catch (InterruptedException e) {
300                                // Interruption, re-check the condition
301                            }
302                         }
303                         if (!hasStarted)
304                            return; // The thread must be stopped immediately.
305                        
306                         // The bundle list is not empty, get the bundle.
307                         // The bundle object is collected inside the synchronized block to avoid
308                         // concurrent modification. However the real process is made outside the
309                         // mutual exclusion area
310                             if(bundles.size()>0)
311                                    bundle = bundles.remove(0);
312                             if(removedBundles.size()>0)
313                                    removeBundle = removedBundles.remove(0);
314                     }
315                     if(bundle!=null){
316                            if(log.isInfoEnabled())
317                                    log.info("Processing AddService for bundle: %s", bundle.getSymbolicName());
318                            parseMetadata(bundle,OSGIConstants.TOPIC_GDS_ADD_SERVICE);
319                     }
320                     if(removeBundle!=null){
321                            if(log.isInfoEnabled())
322                                    log.info("Processing RemoveService for bundle: %s", removeBundle.getSymbolicName());
323                            parseMetadata(removeBundle,OSGIConstants.TOPIC_GDS_REMOVE_SERVICE);
324                     }
325                             }
326                    }
327            }
328             
329    }