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*/
020package org.granite.osgi.metadata;
021
022import java.io.IOException;
023import java.io.InputStream;
024import java.util.ArrayList;
025import java.util.Dictionary;
026import java.util.HashSet;
027import java.util.Hashtable;
028import java.util.List;
029import java.util.Set;
030
031import javax.xml.parsers.DocumentBuilder;
032import javax.xml.parsers.DocumentBuilderFactory;
033import javax.xml.parsers.ParserConfigurationException;
034
035import org.granite.logging.Logger;
036import org.granite.osgi.classloader.ServiceClassLoader;
037import org.granite.osgi.constants.OSGIConstants;
038import org.osgi.framework.Bundle;
039import org.osgi.framework.BundleContext;
040import org.osgi.framework.BundleEvent;
041import org.osgi.framework.SynchronousBundleListener;
042import org.osgi.service.event.Event;
043import org.osgi.service.event.EventAdmin;
044import org.osgi.util.tracker.ServiceTracker;
045import org.w3c.dom.Document;
046import org.w3c.dom.Element;
047import org.w3c.dom.NodeList;
048import 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 */
063public 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}