Posts tagged java
Controller for views in root
0Yesterday while developping ukazbobra I realized, that I don’t know how to map controller for my index view in root of Spring MVC project. I searched and experimented a bit and here is a solution. Please note I’m using Spring Roo and annotation driven setup.
First thing is to create controller, for example simple one like this:
@RequestMapping("/") @Controller public class RootController { @RequestMapping(method = RequestMethod.GET) public String index(Model model) { model.addAttribute("foo", "bar"); return "index"; } . . .
Now you have to turn off thing named ParameterizableViewController, which staticaly selects a view for for rendering. So open up webmvc-config.xml and remove line with mapping for index view:
<mvc:view-controller path="/" view-name="index"/>
That is all, nothing more, nothing less – you have working controller for your index page in root and also for other views.
Komunikace se zabezpecenymi webovymi sluzbami
0Dnes jsem se trapil s tvorbou klienta, ktery se mel pripojit na vzdalenou webovou sluzbu s klientskym certifikatem a predevsim – autentikovat se pomoci jmena a hesla. Nakonec se chyba ukazala byt trochu jinde, nez jsem ji cekal, ale kdyz uz jsem s tim stravil cas, tak to tu trochu popisu, kdyby nahodou nekdo resil podobny problem.
Nejprve ona chyba – mam zdrojak pro klienta vygenerovany pomoci wsimportu a protoze jsem se snazil o univerzalni reseni, adresa koncoveho bodu je v .properties souboru. Lokalne mam ovsem ulozeny wsdl, na ktery se odkazuju pomoci anotace @WebServiceClient ve tride klienta:
@WebServiceClient( name = "myService", targetNamespace = "http://www.shmoula.cz/my/service", wsdlLocation = "META-INF/wsdl/myService.wsdl") public class MyServiceClient extends Service{
No a tohle byl muj problem – toto lokalni wsdl se neshodovalo s deskriptorem na druhe strane (odstranil jsem policy cast), takze mi server porad odmital pozadavky:
javax.xml.ws.WebServiceException: Failed to access the WSDL at: https://shmoula.cz:8080/myService?wsdl. It failed with: Connection refused.
Stacilo zmenit wsdlLocation na adresu deskriptoru na serveru a vse uz bezi v poradku. Hadam, ze kdyby lokalni wsdl presne odpovidalo tomu na serveru, fungovalo by to taky. Ted ale to podstatnejsi – jak autentikovat uzivatele?
Nejprve mejme vygenerovany a naimportovany certifikat v trusted certs, napr. pomoci keytool:
keytool -import -alias shmoula.cz -file certifikat.shmoula.cz.der -keystore <J2EE_HOME>/domains/domain1/config/cacerts.jks
Potom budeme potrebovat vlastni implementaci tridy Authenticator, kterou zajistime, aby byly autentikacni informace predavany pouze pri zabezpecenem spojeni:
package cz.shmoula.klient; import java.net.Authenticator; import java.net.PasswordAuthentication; public class BasicHTTPAuthenticator extends Authenticator { private String userName; private String password; public BasicHTTPAuthenticator(String userName, String password){ this.userName = userName; this.password = password; } @Override protected PasswordAuthentication getPasswordAuthentication(){ if (this.getRequestingProtocol().equalsIgnoreCase("https")) return new PasswordAuthentication(userName, password.toCharArray()); else return null; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
V aplikaci je potom nutne nejprve nastavit tento autentikator a teprve pote vytvorit instanci klienta:
URL baseUrl = cz.shmoula.klient.MyServiceClient.class.getResource("."); URL url = new URL(baseUrl, "https://shmoula.cz:8080/myService?wsdl"); QName qName = new QName("http://www.shmoula.cz/myservice", "myService"); Authenticator.setDefault(new BasicHTTPAuthenticator("jmeno", "heslo")); MyServiceIface service = new MyServiceClient(url, qName).getMyServicePort();
Pote je nutne jeste poupravit kontext pozadavku pomoci BindingProvidera a je mozne se pustit do volani vzdalenych metod, vse zabezpeceno:
BindingProvider bp = (BindingProvider) service; bp.getRequestContext().put(BindingProvider.USERNAME_PROPERTY, "jmeno"); bp.getRequestContext().put(BindingProvider.PASSWORD_PROPERTY, "heslo"); MyServiceResponse resp = service.makeCall(foo, bar);
Abych pravdu rekl, prestava se mi to libit cim dal tim vic, asi se vratim k JSONu. Podstatne je, ze to mam z krku.
Validace vstupu webových služeb
0Potřeboval jsem validovat vstup webové služby na základě xsd šablony – a to jak typy, tak patterny a potřeboval jsem to pokud možno obecně a jednoduše. Postupně jsem se dobral řešení, které se pokusím níže popsat.
Měl jsem zadaný wsdl dokument, k němu definici typů v xsd a příkaz použít jax-ws. Takže jsem provedl wsimport a začal zkoušet. Na komplexní typy byl pomocí jaxb vytvořen binding, takže mi vznikla trochu poschoďovější struktura, která ovšem nebrala v úvahu definované simple typy a prostě je převedla na podobné typy jazyka Java. Pokud jsem tento projekt spustil na serveru, patterny těchto typů prostě zmizely a vygenerované xsd bylo úplně jiné, wsdl se jakž-takž podobalo. Stačilo ovšem přidat parametr k anotaci WebService, který specifikoval cestu k originálnímu wsdl.
@WebService( name = "myServiceSei", targetNamespace = "http://www.shmoula.cz/schema/myservice", wsdlLocation = "META-INF/wsdl/myService.wsdl" )
V tomto okamžiku jsem měl ovšem problém s bindingem, který jsem původně přiřazoval nějaké chybičce ve jmenných prostorech, a ten způsoboval, že mi služba předávala null hodnoty. Nakonec se ukázalo, že v zadaném wsdl se vyskytuje zpráva z názvem doSomeStuffRequest, který se kryl s elementem ve vlastním těle této zprávy (navíc jax-ws přidává postfix Request automaticky), takže stačilo tento název změnit a vše bylo v pořádku a já byl o krok blíže k validování.
První, co mě napadlo, bylo použití anotace SchemaValidation, tak jsem ji použil. Služba se ovšem chovala, jako by tam tato anotace vůbec nebyla a prošlo naprosto všechno. Tudíž nastoupilo řešení číslo dvě – použítí Marshalleru, který by měl při nesrovnalostech skončit s výjimkou. Končil, ovšem s výjimkou jinou, než jsem čekal.
unable to marshal type "cz.foo.bar.DoSomeStuffRequest" as an element because it is missing an @XmlRootElement annotation
Důvody jejího vzniku pěkně rozebral Kohsuke Kawaguchi ve svém blogpostu a protože jsem nechtěl do všech typů cpát XmlRootElement, řídil jsem se jí a dobral se funkčnosti – opravdu mi to validuje! Ještě jsem si s řešením trochu pohrál a především využil vygenerované ObjectFactory, abych nemusel vytvářet vlastní JAXBElement. Výsledek vypadá (zhruba) následovně a funguje na výbornou.
@WebMethod public void doSomeStuff(DoSomeStuffRequest serviceParameters) { ObjectFactory of = new ObjectFactory(); JAXBElement<DoSomeStuffRequest> element = of.createParcelImportRequest(serviceParameters); SchemaFactory sf = SchemaFactory.newInstance(javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI); try { URLClassLoader cl = (URLClassLoader) getClass().getClassLoader(); URL url = cl.findResource("META-INF/wsdl/myService.xsd"); Schema schema = sf.newSchema(url); JAXBContext context = JAXBContext.newInstance(DoSomeStuffRequest.class); Marshaller marshaller = context.createMarshaller(); marshaller.setSchema(schema); marshaller.marshal(element, new DefaultHandler()); } catch (SAXException e) { e.printStackTrace(); } catch (JAXBException e) { e.printStackTrace(); } }
PS. Nic to ovšem nemění na tom, že SOAP není mrtvý, ale nemrtvý (a vůbec že je to fuj)!
Android MapView a onTouch
0Nemálo mě, a nejenom mě, štve absence něčeho, jako je onTouch() v mapách na Androidu, protože potřebuju vytvořit dynamické načítání overlayů. MapView sice má onTouchEvent(), ale že by bylo možné jej použít nějakým mnou zamýšleným způsobem, to zrovna ne. Zkoušel jsem pár způsobů, které tu nastíním a taky jsem zkusil jedno ne zrovna dvakrát moc fér řešení. Ale zoufalí lidé se uchylují k zoufalým věcem, však to znáte… Jen to vezmu hodně hopem, protože mám moře jiných věcí na dělání, ale pár lidem jsem to slíbil a sliby se mají plnit a to nejen o Vánocích ;-).
Zatímco natahování je poměrně jasné (AsyncTask, který bude natahovat data ve čtverci, jehož směrem jsem se posunul – na základě kolize souřadnic tohoto čtverce a viewportu – aby to pořád netahalo jak blbé), taková trivialita jako je onTouch už tak jasná není.
Nejprve jsem se snažil přimět k rozumu metodu onTouchEvent() jak na MapView, tak i na Overlayi, bohužel bezúspěšně. Zajímalo by mě, jestli je tento event pozůstatek něčeho zaniklého, nebo se na něj jenom nějak zapomnělo. Tento způsob by měl fungovat nějak takto, což vypadá poměrně prakticky a použitelně – ve switchi checkovat eventy:
public boolean onTouchEvent(MotionEvent ev) { int action = ev.getAction(); switch (action) { . . . } return super.onTouchEvent(ev); }
Bohužel – z nějakého důvodu tato metoda vůbec nebyla volána. Po nějakém googlení jsem narazil na blog Juriho Strumpflohnera, který řešil stejný problém a vyšpekuloval ho následovně: na mapView navěsit vlastní OnTouchListener(), čili přidat jednoduchou anonymní třídu:
mapView.setOnTouchListener(new OnTouchListener() { public boolean onTouch(View v, MotionEvent ev) { return false; } });
Pokud teda dojde k eventu Touch, jsou procházeny všechny views a případné implementace listenerů v nich. V listeneru implementovaná metoda onTouch je volána nejméně jednou v závislosti na tom, zda vrátí true či false. Pokud vrací false, provede se pouze poprvé a systém je tak chytrý, že už jde příště rovnou na jistotu (aka přímo po metodě, která vrátila true), což je zároveň i kámen úrazu: je nutné zajistit, aby byla volána i metoda „níže“, protože se při true mapa nepohybuje (ale náš onTouch je volaný vždycky). Je teda nutné předat „nižšímu view“ tento event. Nejprve jsem volal dispatchTouchEvent() na view, který přišel do onTouch, ale to je blbost, protože tento view je vlastně onen MapEvent, nad kterým byl navěšen tento listener, takže došlo k zacyklení a StackOverflowError. Takže jsem třídu odanonymizoval a předávám si do ní Context, což je aktivita, uvnitř které je mapView:
public class MyTouchListener implements View.OnTouchListener { private Activity activity; public MyTouchListener(Activity activity) { super(); this.activity = activity; } public boolean onTouch(View v, MotionEvent ev) { activity.dispatchTouchEvent(ev); return true; } }
Taky to ovšem nejede, ale oproti dispatchování do view to vydrží o chvíli déle (a pak to stejně přeteče zásobník a zdechne).
Jak už to bývá, funkční řešení bývají ta nejjednodušší, takže dneska tenhle blogpost konečně dorazím!
Ono totiž stačí rozšířit map view a přepsat v něm onTouchEvent, nebo onInterceptTouchEvent (jak je popsáno tady). V čem je zakopán pudl? V ničem, opravdu to funguje. Předchozí Class Cast Exception se dá zbavit velice jednoduše – definovat správný typ v layoutu! Pojďme tedy na to, nejprve vlastní mapView:
package cz.shmoula.pat; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import com.google.android.maps.MapView; public class AreaMapView extends MapView { private Context mContext; public AreaMapView(Context context, AttributeSet attributeSet) { super(context, attributeSet); mContext = context; } public boolean onTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_MOVE) { Log.i("!!!!!", "Pohyb!"); } return super.onTouchEvent(ev); } }
Teď následuje onen výše zmíněný kámen úrazu, který dříve hulákal Class Cast výjimkou a je to docela logické, ale nevěděl jsem, že je možné vkládat do layoutů vlastní komponenty. Teď už to vím, tak je možné psát něco zhruba takového:
<?xml version="1.0" encoding="utf-8"?> <cz.shmoula.pat.AreaMapView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/areaMapView" android:layout_width="fill_parent" android:layout_height="fill_parent" android:clickable="true" android:apiKey="api_klíč" />
No a v aktivitě se tato komponenta dohledá každému známým způsobem:
AreaMapView mapView = (AreaMapView) findViewById(R.id.areaMapView);
Toť vše! Opravdu, v jednoduchosti je síla. Takže teď přidat nějaké spočítání delta a pokud bylo pohnuto viewportem více, než je výše uvedený čtverec v onu stranu, tak načíst další. Na závěr se ještě musím přiznat, že toto řešení jsem nevyplodil sám, ale sprostě se nechal inspirovat OpenCachingem od Garmina, který mě nakopl na správnou cestu směrem k výsledku. Při pídění se po řešení jsem taky narazil na projekt mapview-overlay-manager, ve kterém se nachází Lazy loading a který si nechávám k dalšímu prostudování a hádám, že nebudu sám.
Testování EJB
0V poslední době jsem byl nucen se věnovat vývoji aplikace s nálepkou Enterprise a to tak, že používám EJB. Možná někdy časem budu moci prozradit, co je to za aplikaci a přidat nějaké informace o tom, jak jsem co navrhoval a realizoval, zatím však budu mlčet jako hrob. Pootevřenej – potřeboval jsem testovat a to bych to zmínit mohl, protože jsem tomu nějaký čas dal.
Pro některé beany (transformátory) bylo možné napsat jednotkové testy, které nepotřebovaly pro svůj běh EJB kontejner – prostě bylo možné vytvořit instanci dané třídy, provést transformaci (či validaci) a podívat se, jaký je výsledek. Pro další služby jsem už ovšem EJB kontejner potřeboval – takže bylo potřeba jej buď spustit, nebo nasimulovat (pěkně to zblognul před mnoha lety finc, který odkazuje na Ejb3unit, ještě dříve krecan odkázal ve spojitosti se Springem na Pitchfork – přiznám se, vyprdl jsem se na oba).
Nejprve jsem uvažoval nad embeded glassfishem, ale zdálo se mi to úchylné, tak jsem rozhodil síť na twitteru a ozval se @dblevins s odkazy, jak je to pácháno na OpenEJB. To jsem zkoušel mimo video je to pěkně popsáno třeba zde), ale končil jsem s
java.lang.ClassFormatError: Absent Code attribute in method that is not native or abstract in class file javax/ejb/embeddable/EJBContainer
což se opakovalo, i když jsem se snažil vygooglit řešení a různě si s tím hrál. Podle všeho jsou knihovny pouze abstrakce a vlastní implementaci vkládá až zvolený kontejner, alespoň tak jsem to pochopil. Takže jsem se ve finále rozhodl jít cestou onoho embedded kontejneru.
Protože vyvíjím pro Glassfishe, padla volba na použití embedded glassfishe. Nejprve jsem se inspiroval Adamem Bienem a pokoušel se vytvořit archiv a ten následně deploynout, což však samozřejmě nebyla správná cesta. Hledal jsem dále a konečně objevil něco funkčního – vytvoření kontejneru se zkompilovanými classami a získání kontextu, nad kterým je možné provádět lookup. Teď bych sepsal kroky, jak jsem to celé zjednodušil a nacpal do ukázkového projektu.
Nejprve si vytvořme nový projekt pomocí mavenu
mvn archetype:create -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=cz.shmoula -DartifactId=ejbTesting
následně je vhodné změnit pom.xml dle výše uvedeného zdroje – přidat závislost a parametry, případně repozitář
. . . <dependency> <groupId>org.glassfish.extras</groupId> <artifactId>glassfish-embedded-all</artifactId> <version>3.0.1</version> <scope>provided</scope> </dependency> . . . <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.0.2</version> <configuration> <source>1.6</source> <target>1.6</target> </configuration> </plugin> </plugins> </build> . . . <repository> <id>glassfish-repository</id> <url>http://download.java.net/maven/glassfish</url> </repository>
Teď je možné přidat například lokální stateless bean a jednoduše jej otestovat, část TestCase je uvedena níže. Za povšimnutí stojí především specifikace cesty, kde se nalézají classes, vytvoření (a uzavření) kontejneru a získání kontextu a dohledání pokusného beanu.
@Override @Before protected void setUp() throws Exception { Map<String, Object> properties = new HashMap<String, Object>(); properties.put(EJBContainer.MODULES, new File("target/classes")); properties.put("org.glassfish.ejb.embedded.glassfish.installation.root", "glassfish"); properties.put(EJBContainer.APP_NAME, "pokus"); container = EJBContainer.createEJBContainer(properties); Context ic = container.getContext(); Object o = ic.lookup("java:global/pokus/Pokus"); if(o instanceof Pokus) this.pokus = (Pokus) o; } @Override @After protected void tearDown() throws Exception { container.close(); } @Test public void testPokus(){ assertNotNull(pokus); } @Test public void testScitani(){ assertEquals(9, pokus.scitani(5, 4)); }
To by mohlo být vše, teď to nějak aplikuju na WS, snad se to tak používá (chci testovat služby na straně serveru) a potom zkusím vkládat Messages na jazyk MDB :-). Nakonec to zas až tak nebolelo, i když mi připadá hodně úchylné ono spouštění embedded kontejneru. Je škoda, že v oficiální dokumentaci se o testování nic nepíše (nebo jsem to přehlédl? prosím link).